First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
import type { GatewayStatus } from "@/features/agents/operations/gatewayRestartPolicy";
|
||||
import type { AgentCreateModalSubmitPayload } from "@/features/agents/creation/types";
|
||||
import type { ConfigMutationKind } from "@/features/agents/operations/useConfigMutationQueue";
|
||||
|
||||
export type MutationKind = "create-agent" | "rename-agent" | "delete-agent";
|
||||
|
||||
export type MutationBlockPhase = "queued" | "mutating" | "awaiting-restart";
|
||||
|
||||
export type MutationBlockState = {
|
||||
kind: MutationKind;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
phase: MutationBlockPhase;
|
||||
startedAt: number;
|
||||
sawDisconnect: boolean;
|
||||
};
|
||||
|
||||
export type MutationStartGuardResult =
|
||||
| { kind: "allow" }
|
||||
| {
|
||||
kind: "deny";
|
||||
reason: "not-connected" | "create-block-active" | "rename-block-active" | "delete-block-active";
|
||||
};
|
||||
|
||||
export const resolveMutationStartGuard = (params: {
|
||||
status: "connected" | "connecting" | "disconnected";
|
||||
hasCreateBlock: boolean;
|
||||
hasRenameBlock: boolean;
|
||||
hasDeleteBlock: boolean;
|
||||
}): MutationStartGuardResult => {
|
||||
if (params.status !== "connected") {
|
||||
return { kind: "deny", reason: "not-connected" };
|
||||
}
|
||||
if (params.hasCreateBlock) {
|
||||
return { kind: "deny", reason: "create-block-active" };
|
||||
}
|
||||
if (params.hasRenameBlock) {
|
||||
return { kind: "deny", reason: "rename-block-active" };
|
||||
}
|
||||
if (params.hasDeleteBlock) {
|
||||
return { kind: "deny", reason: "delete-block-active" };
|
||||
}
|
||||
return { kind: "allow" };
|
||||
};
|
||||
|
||||
export const buildQueuedMutationBlock = (params: {
|
||||
kind: MutationKind;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
startedAt: number;
|
||||
}): MutationBlockState => {
|
||||
return {
|
||||
kind: params.kind,
|
||||
agentId: params.agentId,
|
||||
agentName: params.agentName,
|
||||
phase: "queued",
|
||||
startedAt: params.startedAt,
|
||||
sawDisconnect: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildMutatingMutationBlock = (block: MutationBlockState): MutationBlockState => {
|
||||
return {
|
||||
...block,
|
||||
phase: "mutating",
|
||||
};
|
||||
};
|
||||
|
||||
export type MutationPostRunIntent =
|
||||
| { kind: "clear" }
|
||||
| { kind: "awaiting-restart"; patch: { phase: "awaiting-restart"; sawDisconnect: boolean } };
|
||||
|
||||
export const resolveMutationPostRunIntent = (params: {
|
||||
disposition: "completed" | "awaiting-restart";
|
||||
}): MutationPostRunIntent => {
|
||||
if (params.disposition === "awaiting-restart") {
|
||||
return {
|
||||
kind: "awaiting-restart",
|
||||
patch: {
|
||||
phase: "awaiting-restart",
|
||||
sawDisconnect: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { kind: "clear" };
|
||||
};
|
||||
|
||||
export type MutationSideEffectCommand =
|
||||
| { kind: "reload-agents" }
|
||||
| { kind: "clear-mutation-block" }
|
||||
| { kind: "set-mobile-pane"; pane: "chat" }
|
||||
| { kind: "patch-mutation-block"; patch: { phase: "awaiting-restart"; sawDisconnect: boolean } };
|
||||
|
||||
export const buildMutationSideEffectCommands = (params: {
|
||||
disposition: "completed" | "awaiting-restart";
|
||||
}): MutationSideEffectCommand[] => {
|
||||
const postRunIntent = resolveMutationPostRunIntent({
|
||||
disposition: params.disposition,
|
||||
});
|
||||
if (postRunIntent.kind === "clear") {
|
||||
return [
|
||||
{ kind: "reload-agents" },
|
||||
{ kind: "clear-mutation-block" },
|
||||
{ kind: "set-mobile-pane", pane: "chat" },
|
||||
];
|
||||
}
|
||||
return [{ kind: "patch-mutation-block", patch: postRunIntent.patch }];
|
||||
};
|
||||
|
||||
export type MutationTimeoutIntent =
|
||||
| { kind: "none" }
|
||||
| { kind: "timeout"; reason: "create-timeout" | "rename-timeout" | "delete-timeout" };
|
||||
|
||||
const resolveTimeoutReason = (
|
||||
kind: MutationKind
|
||||
): "create-timeout" | "rename-timeout" | "delete-timeout" => {
|
||||
if (kind === "create-agent") {
|
||||
return "create-timeout";
|
||||
}
|
||||
if (kind === "rename-agent") {
|
||||
return "rename-timeout";
|
||||
}
|
||||
return "delete-timeout";
|
||||
};
|
||||
|
||||
export const resolveMutationTimeoutIntent = (params: {
|
||||
block: MutationBlockState | null;
|
||||
nowMs: number;
|
||||
maxWaitMs: number;
|
||||
}): MutationTimeoutIntent => {
|
||||
if (!params.block) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
const elapsed = params.nowMs - params.block.startedAt;
|
||||
if (elapsed < params.maxWaitMs) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
return {
|
||||
kind: "timeout",
|
||||
reason: resolveTimeoutReason(params.block.kind),
|
||||
};
|
||||
};
|
||||
|
||||
export type MutationWorkflowKind = "rename-agent" | "delete-agent";
|
||||
|
||||
export type MutationWorkflowResult = {
|
||||
disposition: "completed" | "awaiting-restart";
|
||||
};
|
||||
|
||||
export type AwaitingRestartPatch = {
|
||||
phase: "awaiting-restart";
|
||||
sawDisconnect: boolean;
|
||||
};
|
||||
|
||||
export type MutationWorkflowPostRunEffects = {
|
||||
shouldReloadAgents: boolean;
|
||||
shouldClearBlock: boolean;
|
||||
awaitingRestartPatch: AwaitingRestartPatch | null;
|
||||
};
|
||||
|
||||
export type MutationWorkflowDeps = {
|
||||
executeMutation: () => Promise<void>;
|
||||
shouldAwaitRemoteRestart: () => Promise<boolean>;
|
||||
};
|
||||
|
||||
export type AgentConfigMutationLifecycleKind = MutationWorkflowKind;
|
||||
|
||||
export type AgentConfigMutationLifecycleDeps = {
|
||||
enqueueConfigMutation: (params: {
|
||||
kind: ConfigMutationKind;
|
||||
label: string;
|
||||
run: () => Promise<void>;
|
||||
requiresIdleAgents?: boolean;
|
||||
}) => Promise<void>;
|
||||
setQueuedBlock: () => void;
|
||||
setMutatingBlock: () => void;
|
||||
patchBlockAwaitingRestart: (patch: { phase: "awaiting-restart"; sawDisconnect: boolean }) => void;
|
||||
clearBlock: () => void;
|
||||
executeMutation: () => Promise<void>;
|
||||
shouldAwaitRemoteRestart: () => Promise<boolean>;
|
||||
reloadAgents: () => Promise<void>;
|
||||
setMobilePaneChat: () => void;
|
||||
onError: (message: string) => void;
|
||||
};
|
||||
|
||||
export type CreateAgentBlockState = {
|
||||
agentName: string;
|
||||
phase: "queued" | "creating";
|
||||
startedAt: number;
|
||||
};
|
||||
|
||||
export type CreateAgentLifecycleCompletion = {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
};
|
||||
|
||||
export type CreateAgentMutationLifecycleDeps = {
|
||||
enqueueConfigMutation: (params: {
|
||||
kind: ConfigMutationKind;
|
||||
label: string;
|
||||
run: () => Promise<void>;
|
||||
requiresIdleAgents?: boolean;
|
||||
}) => Promise<void>;
|
||||
createAgent: (name: string, avatarSeed: string | null) => Promise<{ id: string }>;
|
||||
setQueuedBlock: (params: { agentName: string; startedAt: number }) => void;
|
||||
setCreatingBlock: (agentName: string) => void;
|
||||
onCompletion: (completion: CreateAgentLifecycleCompletion) => Promise<void> | void;
|
||||
setCreateAgentModalError: (message: string | null) => void;
|
||||
setCreateAgentBusy: (busy: boolean) => void;
|
||||
clearCreateBlock: () => void;
|
||||
onError: (message: string) => void;
|
||||
now?: () => number;
|
||||
};
|
||||
|
||||
export type MutationStatusBlock = {
|
||||
phase: "queued" | "mutating" | "awaiting-restart";
|
||||
sawDisconnect: boolean;
|
||||
};
|
||||
|
||||
type MutationFailureMessageByKind = Record<MutationWorkflowKind, string>;
|
||||
|
||||
const FALLBACK_MUTATION_FAILURE_MESSAGE: MutationFailureMessageByKind = {
|
||||
"rename-agent": "Failed to rename agent.",
|
||||
"delete-agent": "Failed to delete agent.",
|
||||
};
|
||||
|
||||
const assertMutationKind = (kind: string): MutationWorkflowKind => {
|
||||
if (kind === "rename-agent" || kind === "delete-agent") {
|
||||
return kind;
|
||||
}
|
||||
throw new Error(`Unknown config mutation kind: ${kind}`);
|
||||
};
|
||||
|
||||
export const runConfigMutationWorkflow = async (
|
||||
params: { kind: MutationWorkflowKind; isLocalGateway: boolean },
|
||||
deps: MutationWorkflowDeps
|
||||
): Promise<MutationWorkflowResult> => {
|
||||
assertMutationKind(params.kind);
|
||||
await deps.executeMutation();
|
||||
if (params.isLocalGateway) {
|
||||
return { disposition: "completed" };
|
||||
}
|
||||
const shouldAwaitRestart = await deps.shouldAwaitRemoteRestart();
|
||||
return {
|
||||
disposition: shouldAwaitRestart ? "awaiting-restart" : "completed",
|
||||
};
|
||||
};
|
||||
|
||||
export const runAgentConfigMutationLifecycle = async (params: {
|
||||
kind: AgentConfigMutationLifecycleKind;
|
||||
label: string;
|
||||
isLocalGateway: boolean;
|
||||
deps: AgentConfigMutationLifecycleDeps;
|
||||
}): Promise<boolean> => {
|
||||
params.deps.setQueuedBlock();
|
||||
try {
|
||||
await params.deps.enqueueConfigMutation({
|
||||
kind: params.kind,
|
||||
label: params.label,
|
||||
run: async () => {
|
||||
params.deps.setMutatingBlock();
|
||||
const result = await runConfigMutationWorkflow(
|
||||
{ kind: params.kind, isLocalGateway: params.isLocalGateway },
|
||||
{
|
||||
executeMutation: params.deps.executeMutation,
|
||||
shouldAwaitRemoteRestart: params.deps.shouldAwaitRemoteRestart,
|
||||
}
|
||||
);
|
||||
const commands = buildMutationSideEffectCommands({
|
||||
disposition: result.disposition,
|
||||
});
|
||||
for (const command of commands) {
|
||||
if (command.kind === "reload-agents") {
|
||||
await params.deps.reloadAgents();
|
||||
continue;
|
||||
}
|
||||
if (command.kind === "clear-mutation-block") {
|
||||
params.deps.clearBlock();
|
||||
continue;
|
||||
}
|
||||
if (command.kind === "set-mobile-pane") {
|
||||
params.deps.setMobilePaneChat();
|
||||
continue;
|
||||
}
|
||||
params.deps.patchBlockAwaitingRestart(command.patch);
|
||||
}
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
params.deps.clearBlock();
|
||||
params.deps.onError(
|
||||
buildConfigMutationFailureMessage({
|
||||
kind: params.kind,
|
||||
error,
|
||||
})
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const runCreateAgentMutationLifecycle = async (
|
||||
params: {
|
||||
payload: AgentCreateModalSubmitPayload;
|
||||
status: "connected" | "connecting" | "disconnected";
|
||||
hasCreateBlock: boolean;
|
||||
hasRenameBlock: boolean;
|
||||
hasDeleteBlock: boolean;
|
||||
createAgentBusy: boolean;
|
||||
},
|
||||
deps: CreateAgentMutationLifecycleDeps
|
||||
): Promise<boolean> => {
|
||||
if (params.createAgentBusy) return false;
|
||||
const guard = resolveMutationStartGuard({
|
||||
status: params.status,
|
||||
hasCreateBlock: params.hasCreateBlock,
|
||||
hasRenameBlock: params.hasRenameBlock,
|
||||
hasDeleteBlock: params.hasDeleteBlock,
|
||||
});
|
||||
if (guard.kind === "deny") {
|
||||
if (guard.reason === "not-connected") {
|
||||
deps.setCreateAgentModalError("Connect to gateway before creating an agent.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const name = params.payload.name.trim();
|
||||
if (!name) {
|
||||
deps.setCreateAgentModalError("Agent name is required.");
|
||||
return false;
|
||||
}
|
||||
|
||||
deps.setCreateAgentBusy(true);
|
||||
deps.setCreateAgentModalError(null);
|
||||
const startedAt = (deps.now ?? Date.now)();
|
||||
deps.setQueuedBlock({ agentName: name, startedAt });
|
||||
const avatarSeed = params.payload.avatarSeed?.trim() ?? null;
|
||||
try {
|
||||
const queuedMutation = deps.enqueueConfigMutation({
|
||||
kind: "create-agent",
|
||||
label: `Create ${name}`,
|
||||
run: async () => {
|
||||
deps.setCreatingBlock(name);
|
||||
const created = await deps.createAgent(name, avatarSeed);
|
||||
await deps.onCompletion({
|
||||
agentId: created.id,
|
||||
agentName: name,
|
||||
});
|
||||
},
|
||||
});
|
||||
await queuedMutation;
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to create agent.";
|
||||
deps.clearCreateBlock();
|
||||
deps.setCreateAgentModalError(message);
|
||||
deps.onError(message);
|
||||
return false;
|
||||
} finally {
|
||||
deps.setCreateAgentBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
export const isCreateBlockTimedOut = (params: {
|
||||
block: CreateAgentBlockState | null;
|
||||
nowMs: number;
|
||||
maxWaitMs: number;
|
||||
}): boolean => {
|
||||
if (!params.block || params.block.phase === "queued") {
|
||||
return false;
|
||||
}
|
||||
const timeoutIntent = resolveMutationTimeoutIntent({
|
||||
block: {
|
||||
kind: "create-agent",
|
||||
agentId: "",
|
||||
agentName: params.block.agentName,
|
||||
phase: "mutating",
|
||||
startedAt: params.block.startedAt,
|
||||
sawDisconnect: false,
|
||||
},
|
||||
nowMs: params.nowMs,
|
||||
maxWaitMs: params.maxWaitMs,
|
||||
});
|
||||
return timeoutIntent.kind === "timeout" && timeoutIntent.reason === "create-timeout";
|
||||
};
|
||||
|
||||
export const buildConfigMutationFailureMessage = (params: {
|
||||
kind: MutationWorkflowKind;
|
||||
error: unknown;
|
||||
}): string => {
|
||||
const fallback = FALLBACK_MUTATION_FAILURE_MESSAGE[params.kind];
|
||||
if (params.error instanceof Error) {
|
||||
return params.error.message || fallback;
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const resolveConfigMutationStatusLine = (params: {
|
||||
block: MutationStatusBlock | null;
|
||||
status: GatewayStatus;
|
||||
mutatingLabel?: string;
|
||||
}): string | null => {
|
||||
const { block, status } = params;
|
||||
if (!block) return null;
|
||||
if (block.phase === "queued") {
|
||||
return "Waiting for active runs to finish";
|
||||
}
|
||||
if (block.phase === "mutating") {
|
||||
return params.mutatingLabel ?? "Submitting config change";
|
||||
}
|
||||
if (!block.sawDisconnect) {
|
||||
return "Waiting for gateway to restart";
|
||||
}
|
||||
return status === "connected"
|
||||
? "Gateway is back online, syncing agents"
|
||||
: "Gateway restart in progress";
|
||||
};
|
||||
|
||||
export const buildAwaitingRestartPatch = (): AwaitingRestartPatch => {
|
||||
return {
|
||||
phase: "awaiting-restart",
|
||||
sawDisconnect: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveConfigMutationPostRunEffects = (
|
||||
result: MutationWorkflowResult
|
||||
): MutationWorkflowPostRunEffects => {
|
||||
const commands = buildMutationSideEffectCommands({
|
||||
disposition: result.disposition,
|
||||
});
|
||||
let shouldReloadAgents = false;
|
||||
let shouldClearBlock = false;
|
||||
let awaitingRestartPatch: AwaitingRestartPatch | null = null;
|
||||
for (const command of commands) {
|
||||
if (command.kind === "reload-agents") {
|
||||
shouldReloadAgents = true;
|
||||
continue;
|
||||
}
|
||||
if (command.kind === "clear-mutation-block") {
|
||||
shouldClearBlock = true;
|
||||
continue;
|
||||
}
|
||||
if (command.kind === "patch-mutation-block") {
|
||||
awaitingRestartPatch = command.patch;
|
||||
}
|
||||
}
|
||||
return {
|
||||
shouldReloadAgents,
|
||||
shouldClearBlock,
|
||||
awaitingRestartPatch,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user