Files
openclaw/src/agents/subagent-spawn-session-patch.ts
2026-07-27 02:20:56 -04:00

199 lines
7.6 KiB
TypeScript

import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { buildSessionCreationStamp } from "../config/sessions/session-entry-provenance.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveIncognitoOpenClawAgentSqlitePath } from "../state/openclaw-agent-db.js";
import {
inheritedToolAllowPatch,
inheritedToolDenyPatch,
normalizeInheritedToolAllowlist,
normalizeInheritedToolDenylist,
} from "./inherited-tool-deny.js";
import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js";
import { splitModelRef } from "./subagent-spawn-plan.js";
import { resolveGatewaySessionStoreTarget, upsertSessionEntry } from "./subagent-spawn.runtime.js";
function buildDirectChildSessionPatch(patch: Record<string, unknown>): Partial<SessionEntry> {
const entry: Partial<SessionEntry> = {};
const spawnDepth = patch.spawnDepth;
if (typeof spawnDepth === "number" && Number.isFinite(spawnDepth) && spawnDepth >= 0) {
entry.spawnDepth = Math.floor(spawnDepth);
}
if (patch.subagentRole === "orchestrator" || patch.subagentRole === "leaf") {
entry.subagentRole = patch.subagentRole;
}
if (patch.subagentControlScope === "children" || patch.subagentControlScope === "none") {
entry.subagentControlScope = patch.subagentControlScope;
}
if (patch.inheritedToolPolicyVersion === 1) {
entry.inheritedToolPolicyVersion = 1;
}
if (patch.incognito === true) {
entry.incognito = true;
}
if (typeof patch.spawnedBy === "string" && patch.spawnedBy.trim()) {
entry.spawnedBy = patch.spawnedBy.trim();
}
if (
typeof patch.completionOwnerSessionKey === "string" &&
patch.completionOwnerSessionKey.trim()
) {
entry.completionOwnerSessionKey = patch.completionOwnerSessionKey.trim();
}
if (typeof patch.parentSessionKey === "string" && patch.parentSessionKey.trim()) {
entry.parentSessionKey = patch.parentSessionKey.trim();
}
if (typeof patch.spawnedWorkspaceDir === "string" && patch.spawnedWorkspaceDir.trim()) {
entry.spawnedWorkspaceDir = patch.spawnedWorkspaceDir.trim();
}
if (typeof patch.spawnedCwd === "string" && patch.spawnedCwd.trim()) {
entry.spawnedCwd = patch.spawnedCwd.trim();
}
const inheritedToolDeny = normalizeInheritedToolDenylist(patch.inheritedToolDeny);
if (inheritedToolDeny.length > 0) {
entry.inheritedToolDeny = inheritedToolDeny;
}
const inheritedToolAllow = normalizeInheritedToolAllowlist(patch.inheritedToolAllow);
if (inheritedToolAllow.length > 0) {
entry.inheritedToolAllow = inheritedToolAllow;
}
if (typeof patch.thinkingLevel === "string" && patch.thinkingLevel.trim()) {
entry.thinkingLevel = patch.thinkingLevel.trim();
}
if (patch.fastMode === true || patch.fastMode === false || patch.fastMode === "auto") {
entry.fastMode = patch.fastMode;
}
if (typeof patch.swarmGroupId === "string" && patch.swarmGroupId.trim()) {
entry.swarmGroupId = patch.swarmGroupId.trim();
}
if (patch.swarmCollector === true) {
entry.swarmCollector = true;
}
if (patch.swarmOutputSchema && typeof patch.swarmOutputSchema === "object") {
entry.swarmOutputSchema = patch.swarmOutputSchema as Record<string, unknown>;
}
if (typeof patch.model === "string" && patch.model.trim()) {
const { provider, model } = splitModelRef(patch.model.trim());
if (model) {
entry.model = model;
entry.modelOverride = model;
entry.modelOverrideSource = patch.modelOverrideSource === "auto" ? "auto" : "user";
entry.modelOverrideRouteResolution = "resolved";
const fallbackOriginProvider = normalizeOptionalString(
patch.modelOverrideFallbackOriginProvider,
);
const fallbackOriginModel = normalizeOptionalString(patch.modelOverrideFallbackOriginModel);
if (fallbackOriginProvider && fallbackOriginModel) {
entry.modelOverrideFallbackOriginProvider = fallbackOriginProvider;
entry.modelOverrideFallbackOriginModel = fallbackOriginModel;
}
if (provider) {
entry.modelProvider = provider;
entry.providerOverride = provider;
}
}
}
return entry;
}
export function loadSubagentConfig() {
return getSubagentSpawnDeps().getRuntimeConfig();
}
export async function createInitialSubagentSession(params: {
cfg: OpenClawConfig;
targetAgentId: string;
childSessionKey: string;
incognito: boolean;
requesterInternalKey: string;
completionOwnerSessionKey: string;
spawnedWorkspaceDir?: string;
spawnedCwd?: string;
admissionPatch?: Record<string, unknown>;
inheritedToolAllowlist?: string[];
inheritedToolDenylist?: string[];
modelPatch: Record<string, unknown>;
swarmGroupId?: string;
collect: boolean;
outputSchema?: Record<string, unknown>;
}): Promise<{ status: "ok"; entry?: SessionEntry } | { status: "error"; error: string }> {
const initialChildSessionPatch: Record<string, unknown> = {
spawnedBy: params.requesterInternalKey,
completionOwnerSessionKey: params.completionOwnerSessionKey,
// Navigation and control lineage commit with the creation stamp so a
// launch failure cannot leave a durable but parentless child row.
parentSessionKey: params.requesterInternalKey,
...(params.spawnedWorkspaceDir ? { spawnedWorkspaceDir: params.spawnedWorkspaceDir } : {}),
...(params.spawnedCwd ? { spawnedCwd: params.spawnedCwd } : {}),
...params.admissionPatch,
inheritedToolPolicyVersion: 1,
...inheritedToolAllowPatch(params.inheritedToolAllowlist),
...inheritedToolDenyPatch(params.inheritedToolDenylist),
...params.modelPatch,
...(params.swarmGroupId ? { swarmGroupId: params.swarmGroupId } : {}),
...(params.collect ? { swarmCollector: true } : {}),
...(params.outputSchema ? { swarmOutputSchema: params.outputSchema } : {}),
...(params.incognito ? { incognito: true } : {}),
};
try {
const target = params.incognito
? {
agentId: params.targetAgentId,
canonicalKey: params.childSessionKey,
storeKeys: [params.childSessionKey],
storePath: resolveIncognitoOpenClawAgentSqlitePath({ agentId: params.targetAgentId }),
}
: resolveGatewaySessionStoreTarget({
cfg: params.cfg,
key: params.childSessionKey,
});
const entry = await upsertSessionEntry(
{
storePath: target.storePath,
sessionKey: target.canonicalKey,
},
{
...buildDirectChildSessionPatch(initialChildSessionPatch),
...buildSessionCreationStamp({
via: "spawn",
actor: { type: "agent", id: params.requesterInternalKey },
}),
},
);
return { status: "ok", entry: entry ?? undefined };
} catch (err) {
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "error";
return { status: "error", error: `child session patch failed: ${message}` };
}
}
export async function persistInitialChildSessionRuntimeModel(params: {
cfg: OpenClawConfig;
childSessionKey: string;
resolvedModel?: string;
}): Promise<string | undefined> {
const { provider, model } = splitModelRef(params.resolvedModel);
if (!model) {
return undefined;
}
try {
const target = resolveGatewaySessionStoreTarget({
cfg: params.cfg,
key: params.childSessionKey,
});
await upsertSessionEntry(
{
storePath: target.storePath,
sessionKey: target.canonicalKey,
},
{
model,
...(provider ? { modelProvider: provider } : {}),
},
);
return undefined;
} catch (err) {
return err instanceof Error ? err.message : typeof err === "string" ? err : "error";
}
}