pilotswarm-sdk 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/protocol.js +7 -0
- package/dist/blob-store.d.ts +10 -1
- package/dist/blob-store.d.ts.map +1 -1
- package/dist/blob-store.js +275 -13
- package/dist/blob-store.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -1
- package/dist/client.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +164 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +6 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +3 -0
- package/dist/cms.js.map +1 -1
- package/dist/fault-injection.d.ts +2 -0
- package/dist/fault-injection.d.ts.map +1 -0
- package/dist/fault-injection.js +45 -0
- package/dist/fault-injection.js.map +1 -0
- package/dist/orchestration/index.d.ts +3 -3
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts +9 -2
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +21 -55
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/state.d.ts +7 -3
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +13 -4
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +98 -66
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +6 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +4 -4
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_56/agents.d.ts +41 -0
- package/dist/orchestration_1_0_56/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/agents.js +758 -0
- package/dist/orchestration_1_0_56/agents.js.map +1 -0
- package/dist/orchestration_1_0_56/index.d.ts +24 -0
- package/dist/orchestration_1_0_56/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/index.js +13 -0
- package/dist/orchestration_1_0_56/index.js.map +1 -0
- package/dist/orchestration_1_0_56/lifecycle.d.ts +41 -0
- package/dist/orchestration_1_0_56/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/lifecycle.js +580 -0
- package/dist/orchestration_1_0_56/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_56/queue.d.ts +7 -0
- package/dist/orchestration_1_0_56/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/queue.js +644 -0
- package/dist/orchestration_1_0_56/queue.js.map +1 -0
- package/dist/orchestration_1_0_56/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_56/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/runtime.js +194 -0
- package/dist/orchestration_1_0_56/runtime.js.map +1 -0
- package/dist/orchestration_1_0_56/state.d.ts +126 -0
- package/dist/orchestration_1_0_56/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/state.js +106 -0
- package/dist/orchestration_1_0_56/state.js.map +1 -0
- package/dist/orchestration_1_0_56/turn.d.ts +30 -0
- package/dist/orchestration_1_0_56/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/turn.js +1075 -0
- package/dist/orchestration_1_0_56/turn.js.map +1 -0
- package/dist/orchestration_1_0_56/utils.d.ts +22 -0
- package/dist/orchestration_1_0_56/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_56/utils.js +226 -0
- package/dist/orchestration_1_0_56/utils.js.map +1 -0
- package/dist/orchestration_1_0_57/agents.d.ts +41 -0
- package/dist/orchestration_1_0_57/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/agents.js +758 -0
- package/dist/orchestration_1_0_57/agents.js.map +1 -0
- package/dist/orchestration_1_0_57/index.d.ts +24 -0
- package/dist/orchestration_1_0_57/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/index.js +13 -0
- package/dist/orchestration_1_0_57/index.js.map +1 -0
- package/dist/orchestration_1_0_57/lifecycle.d.ts +49 -0
- package/dist/orchestration_1_0_57/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/lifecycle.js +583 -0
- package/dist/orchestration_1_0_57/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_57/queue.d.ts +7 -0
- package/dist/orchestration_1_0_57/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/queue.js +644 -0
- package/dist/orchestration_1_0_57/queue.js.map +1 -0
- package/dist/orchestration_1_0_57/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_57/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/runtime.js +198 -0
- package/dist/orchestration_1_0_57/runtime.js.map +1 -0
- package/dist/orchestration_1_0_57/state.d.ts +133 -0
- package/dist/orchestration_1_0_57/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/state.js +115 -0
- package/dist/orchestration_1_0_57/state.js.map +1 -0
- package/dist/orchestration_1_0_57/turn.d.ts +30 -0
- package/dist/orchestration_1_0_57/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/turn.js +1110 -0
- package/dist/orchestration_1_0_57/turn.js.map +1 -0
- package/dist/orchestration_1_0_57/utils.d.ts +22 -0
- package/dist/orchestration_1_0_57/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_57/utils.js +226 -0
- package/dist/orchestration_1_0_57/utils.js.map +1 -0
- package/dist/session-lifecycle.d.ts +80 -0
- package/dist/session-lifecycle.d.ts.map +1 -0
- package/dist/session-lifecycle.js +257 -0
- package/dist/session-lifecycle.js.map +1 -0
- package/dist/session-manager.d.ts +21 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +137 -1
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +5 -1
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +1113 -957
- package/dist/session-proxy.js.map +1 -1
- package/dist/session-store.d.ts +48 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +394 -28
- package/dist/session-store.js.map +1 -1
- package/dist/snapshot-protocol.d.ts +101 -0
- package/dist/snapshot-protocol.d.ts.map +1 -0
- package/dist/snapshot-protocol.js +120 -0
- package/dist/snapshot-protocol.js.map +1 -0
- package/dist/types.d.ts +24 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wait-affinity.d.ts +11 -0
- package/dist/wait-affinity.d.ts.map +1 -1
- package/dist/wait-affinity.js +6 -0
- package/dist/wait-affinity.js.map +1 -1
- package/dist/web/web-management-client.d.ts +28 -9
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +30 -15
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts +16 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +63 -6
- package/dist/worker.js.map +1 -1
- package/package.json +7 -4
package/dist/session-proxy.js
CHANGED
|
@@ -13,7 +13,10 @@ import { computeCronAtNextFire } from "./cron-at.js";
|
|
|
13
13
|
import { SpanStatusCode, trace as otelTrace } from "@opentelemetry/api";
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
16
17
|
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { attemptStoreRecovery, runTurnCommit, runTurnPreamble } from "./session-lifecycle.js";
|
|
19
|
+
import { supportsVersionedSnapshots, writeTurnSentinel } from "./snapshot-protocol.js";
|
|
17
20
|
const SYSTEM_AGENT_IDS = new Set(["pilotswarm", "sweeper", "resourcemgr", "facts-manager", "agent-tuner"]);
|
|
18
21
|
const SESSION_RECOVERY_NOTICE = "[SYSTEM: The runtime recovered this session after the live Copilot session was lost on a worker. " +
|
|
19
22
|
"Some very recent in-memory state may have been lost. Re-read the visible conversation and continue carefully from the latest durable state.]";
|
|
@@ -23,6 +26,8 @@ const LOSSY_SESSION_REPLAY_NOTICE = "[SYSTEM: The runtime is replaying this turn
|
|
|
23
26
|
const CORRUPTED_TRANSCRIPT_REPLAY_NOTICE = "[SYSTEM: The runtime recreated this session after the live Copilot transcript became inconsistent. " +
|
|
24
27
|
"Some recent in-memory work may be missing, and the previous turn may have partially executed. " +
|
|
25
28
|
"Re-read the visible conversation and continue carefully from the latest durable state.]";
|
|
29
|
+
const REHYDRATED_SESSION_NOTICE = "[SYSTEM: This session was rehydrated on a different worker from its durable snapshot. " +
|
|
30
|
+
"The conversation history is fully preserved. Continue seamlessly.]";
|
|
26
31
|
function normalizeJsonObject(value) {
|
|
27
32
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
28
33
|
return null;
|
|
@@ -429,6 +434,7 @@ export function createSessionProxy(ctx, sessionId, affinityKey, config) {
|
|
|
429
434
|
...(turnMeta?.clientMessageIds && turnMeta.clientMessageIds.length > 0
|
|
430
435
|
? { clientMessageIds: turnMeta.clientMessageIds }
|
|
431
436
|
: {}),
|
|
437
|
+
...(turnMeta?.snapshot ? { snapshot: turnMeta.snapshot } : {}),
|
|
432
438
|
}, affinityKey);
|
|
433
439
|
},
|
|
434
440
|
dehydrate(reason, eventData) {
|
|
@@ -618,7 +624,7 @@ export function createSessionManagerProxy(ctx) {
|
|
|
618
624
|
// ─── Activity Registration ───────────────────────────────────────
|
|
619
625
|
// Thin dispatchers — each is a one-liner that calls the corresponding
|
|
620
626
|
// SessionManager or ManagedSession method.
|
|
621
|
-
export function registerActivities(runtime, sessionManager,
|
|
627
|
+
export function registerActivities(runtime, sessionManager, sessionStore, githubToken, catalog, provider, storeUrl, cmsSchema, clientConfig,
|
|
622
628
|
/** Loaded system agents — used by resolveAgentConfig activity. */
|
|
623
629
|
systemAgents,
|
|
624
630
|
/** Worker-level session policy — used by getWorkerSessionPolicy activity. */
|
|
@@ -715,1024 +721,1159 @@ workerNodeId) {
|
|
|
715
721
|
let finalTurnResult = null;
|
|
716
722
|
try {
|
|
717
723
|
finalTurnResult = await sessionManager.withRunTurnLock(input.sessionId, "runTurn", async () => {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
724
|
+
// ── Session lifecycle protocol preamble (proposal §3.3) ─────────
|
|
725
|
+
// Only active when the orchestration (1.0.57+) supplied snapshot
|
|
726
|
+
// coordinates AND the store implements the versioned CAS contract.
|
|
727
|
+
// Absent either, this activity behaves exactly as before.
|
|
728
|
+
const lifecycle = input.snapshot && sessionStore && supportsVersionedSnapshots(sessionStore)
|
|
729
|
+
? {
|
|
730
|
+
store: sessionStore,
|
|
731
|
+
sessionStateDir: sessionManager.getSessionStateDir(),
|
|
732
|
+
sessionId: input.sessionId,
|
|
733
|
+
expectedVersion: input.snapshot.expectedVersion,
|
|
734
|
+
turnKey: input.snapshot.turnKey,
|
|
735
|
+
dropWarmSession: () => sessionManager.dropWarmSession(input.sessionId),
|
|
723
736
|
trace,
|
|
724
|
-
lockHeld: true,
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
catch (err) {
|
|
728
|
-
const message = err?.message || String(err);
|
|
729
|
-
if (isMissingSessionStateErrorMessage(message) || isLiveSessionLostErrorMessage(message)) {
|
|
730
|
-
const detail = isMissingSessionStateErrorMessage(message)
|
|
731
|
-
? stripMissingSessionStatePrefix(message)
|
|
732
|
-
: message;
|
|
733
|
-
trace(`session=${input.sessionId} missing resumable state before turn ${input.turnIndex ?? "unknown"}; ` +
|
|
734
|
-
"starting lossy fresh-session replay");
|
|
735
|
-
await recordLossyHandoffEvent(catalog, input.sessionId, workerNodeId, {
|
|
736
|
-
cause: "missing_resumable_state_before_run_turn",
|
|
737
|
-
message: buildLossyReplayMessage(input.sessionId, detail),
|
|
738
|
-
detail,
|
|
739
|
-
error: message,
|
|
740
|
-
recoveryMode: "fresh_session_replay",
|
|
741
|
-
nextStep: "replay_pending_turn_with_recreated_copilot_session",
|
|
742
|
-
...(input.turnIndex != null ? { iteration: input.turnIndex } : {}),
|
|
743
|
-
}, (failureMessage) => activityCtx.traceInfo(`[runTurn] ${failureMessage}`));
|
|
744
|
-
if (catalog) {
|
|
745
|
-
await cmsRetryBestEffort(`runTurn.recordEvent system.message-lossy-replay session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
746
|
-
eventType: "system.message",
|
|
747
|
-
data: {
|
|
748
|
-
content: "The runtime is replaying this turn after a worker restart lost the live Copilot session state before durable dehydrate completed. " +
|
|
749
|
-
"Some recent work may be missing or partially executed.",
|
|
750
|
-
},
|
|
751
|
-
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
752
|
-
}
|
|
753
|
-
try {
|
|
754
|
-
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
755
|
-
turnIndex: 0,
|
|
756
|
-
trace,
|
|
757
|
-
lockHeld: true,
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
catch (recoveryErr) {
|
|
761
|
-
const recoveryMessage = recoveryErr?.message || String(recoveryErr);
|
|
762
|
-
const fatalMessage = isMissingSessionStateErrorMessage(recoveryMessage)
|
|
763
|
-
? buildUnrecoverableSessionLossMessage(input.sessionId, stripMissingSessionStatePrefix(recoveryMessage))
|
|
764
|
-
: buildUnrecoverableSessionLossMessage(input.sessionId, recoveryMessage);
|
|
765
|
-
trace(`session=${input.sessionId} lossy replay session recreation failed error=${fatalMessage}`);
|
|
766
|
-
return await failForMissingState(fatalMessage);
|
|
767
|
-
}
|
|
768
|
-
effectivePrompt = mergePromptSections([LOSSY_SESSION_REPLAY_NOTICE, input.prompt]) || input.prompt;
|
|
769
737
|
}
|
|
770
|
-
|
|
771
|
-
|
|
738
|
+
: null;
|
|
739
|
+
let lifecycleBaseVersion = 0;
|
|
740
|
+
let lifecycleRehydrated = false;
|
|
741
|
+
// Protocol-native persistence stats: the legacy dehydrate/hydrate
|
|
742
|
+
// activities no longer run, so the preamble/commit paths feed the
|
|
743
|
+
// same CMS summary + events the Stats pane reads. Best-effort only.
|
|
744
|
+
const recordLifecycleHydration = async (version) => {
|
|
745
|
+
if (!catalog)
|
|
746
|
+
return;
|
|
747
|
+
await cmsRetryBestEffort(`runTurn.lifecycleHydrated session=${input.sessionId}`, async () => {
|
|
748
|
+
await catalog.upsertSessionMetricSummary(input.sessionId, {
|
|
749
|
+
hydrationCountIncrement: 1,
|
|
750
|
+
lastHydratedAt: true,
|
|
751
|
+
});
|
|
752
|
+
await catalog.recordEvents(input.sessionId, [{
|
|
753
|
+
eventType: "session.hydrated",
|
|
754
|
+
data: { version, protocol: "lifecycle" },
|
|
755
|
+
}], workerNodeId);
|
|
756
|
+
}, (msg) => activityCtx.traceInfo(msg));
|
|
757
|
+
};
|
|
758
|
+
if (lifecycle) {
|
|
759
|
+
const pre = await runTurnPreamble(lifecycle);
|
|
760
|
+
if (pre.kind === "already-committed") {
|
|
761
|
+
activityCtx.traceInfo(`[runTurn] session=${input.sessionId} already-committed recovery at v${pre.version}; ` +
|
|
762
|
+
`returning stored result without re-running the turn`);
|
|
763
|
+
await recordLifecycleHydration(pre.version);
|
|
764
|
+
return { ...pre.result, snapshotVersion: pre.version };
|
|
765
|
+
}
|
|
766
|
+
lifecycleBaseVersion = pre.baseVersion;
|
|
767
|
+
lifecycleRehydrated = pre.kind === "hydrated";
|
|
768
|
+
if (lifecycleRehydrated) {
|
|
769
|
+
await recordLifecycleHydration(pre.baseVersion);
|
|
770
|
+
}
|
|
771
|
+
if (pre.kind === "fresh" && pre.lossy && catalog) {
|
|
772
|
+
await cmsRetryBestEffort(`runTurn.recordEvent snapshot-store-empty session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
773
|
+
eventType: "session.snapshot_store_empty",
|
|
774
|
+
data: {
|
|
775
|
+
expectedVersion: input.snapshot.expectedVersion,
|
|
776
|
+
message: "Snapshot store held no data for a session with committed turns; falling back to fresh-session recovery.",
|
|
777
|
+
},
|
|
778
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
772
779
|
}
|
|
773
780
|
}
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (inlineSdkClientPromise)
|
|
778
|
-
return await inlineSdkClientPromise;
|
|
779
|
-
if (!storeUrl)
|
|
780
|
-
throw new Error("No storeUrl — cannot create PilotSwarmClient");
|
|
781
|
-
inlineSdkClientPromise = (async () => {
|
|
782
|
-
const startedClient = new PilotSwarmClient(internalClientConfig());
|
|
783
|
-
await startedClient.start();
|
|
784
|
-
inlineSdkClient = startedClient;
|
|
785
|
-
return startedClient;
|
|
786
|
-
})();
|
|
781
|
+
const executeTurnBody = async () => {
|
|
782
|
+
let session = null;
|
|
783
|
+
let effectivePrompt = input.prompt;
|
|
787
784
|
try {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
};
|
|
794
|
-
const isAutonomousSystemTurn = () => {
|
|
795
|
-
if (!SYSTEM_AGENT_IDS.has(runConfig.agentIdentity || ""))
|
|
796
|
-
return false;
|
|
797
|
-
if (input.bootstrap === true)
|
|
798
|
-
return true;
|
|
799
|
-
return /^\s*\[SYSTEM:/i.test(String(input.prompt ?? ""));
|
|
800
|
-
};
|
|
801
|
-
const sanitizeAutonomousSystemSessionFilters = (args) => {
|
|
802
|
-
if (!args || !isAutonomousSystemTurn())
|
|
803
|
-
return args;
|
|
804
|
-
if (!args.owner_query && !args.owner_kind)
|
|
805
|
-
return args;
|
|
806
|
-
const sanitized = { ...args };
|
|
807
|
-
delete sanitized.owner_query;
|
|
808
|
-
delete sanitized.owner_kind;
|
|
809
|
-
return sanitized;
|
|
810
|
-
};
|
|
811
|
-
const isReadOnlyTuner = () => runConfig.agentIdentity === "agent-tuner";
|
|
812
|
-
const normalizeAgentLookup = (value) => (value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
813
|
-
const resolveAgentConfigInline = (agentName) => {
|
|
814
|
-
const agents = [
|
|
815
|
-
...(userAgents ?? []).map(a => ({ ...a, system: false, creatable: true })),
|
|
816
|
-
...(systemAgents ?? []).map(a => ({ ...a, creatable: false })),
|
|
817
|
-
];
|
|
818
|
-
let lookupNamespace;
|
|
819
|
-
let rawName = agentName;
|
|
820
|
-
if (agentName.includes(":")) {
|
|
821
|
-
const parts = agentName.split(":");
|
|
822
|
-
lookupNamespace = parts[0];
|
|
823
|
-
rawName = parts.slice(1).join(":");
|
|
785
|
+
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
786
|
+
turnIndex: input.turnIndex,
|
|
787
|
+
trace,
|
|
788
|
+
lockHeld: true,
|
|
789
|
+
});
|
|
824
790
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
791
|
+
catch (err) {
|
|
792
|
+
const message = err?.message || String(err);
|
|
793
|
+
if (isMissingSessionStateErrorMessage(message) || isLiveSessionLostErrorMessage(message)) {
|
|
794
|
+
const detail = isMissingSessionStateErrorMessage(message)
|
|
795
|
+
? stripMissingSessionStatePrefix(message)
|
|
796
|
+
: message;
|
|
797
|
+
// ── Lifecycle store recovery BEFORE lossy replay ─────────
|
|
798
|
+
// The store may hold a perfect committed snapshot (e.g. the
|
|
799
|
+
// SDK refused to resume from locally damaged files). Restore
|
|
800
|
+
// it and retry — lossy replay (whose turn-0 reset DELETES
|
|
801
|
+
// the store snapshot) is only reachable when the store is
|
|
802
|
+
// empty, making that delete a no-op.
|
|
803
|
+
if (lifecycle) {
|
|
804
|
+
try {
|
|
805
|
+
const recoveredVersion = await attemptStoreRecovery(lifecycle);
|
|
806
|
+
if (recoveredVersion != null) {
|
|
807
|
+
trace(`session=${input.sessionId} restored committed snapshot v${recoveredVersion} ` +
|
|
808
|
+
`after getOrCreate failure (${detail.slice(0, 120)}); retrying resume`);
|
|
809
|
+
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
810
|
+
turnIndex: input.turnIndex,
|
|
811
|
+
trace,
|
|
812
|
+
lockHeld: true,
|
|
813
|
+
});
|
|
814
|
+
lifecycleBaseVersion = recoveredVersion;
|
|
815
|
+
lifecycleRehydrated = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch (storeRecoveryErr) {
|
|
819
|
+
trace(`session=${input.sessionId} snapshot-store recovery failed: ` +
|
|
820
|
+
`${storeRecoveryErr?.message ?? storeRecoveryErr}; falling back to lossy replay`);
|
|
821
|
+
session = null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (session) {
|
|
825
|
+
// Recovered losslessly — skip the lossy replay machinery.
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
trace(`session=${input.sessionId} missing resumable state before turn ${input.turnIndex ?? "unknown"}; ` +
|
|
829
|
+
"starting lossy fresh-session replay");
|
|
830
|
+
await recordLossyHandoffEvent(catalog, input.sessionId, workerNodeId, {
|
|
831
|
+
cause: "missing_resumable_state_before_run_turn",
|
|
832
|
+
message: buildLossyReplayMessage(input.sessionId, detail),
|
|
833
|
+
detail,
|
|
834
|
+
error: message,
|
|
835
|
+
recoveryMode: "fresh_session_replay",
|
|
836
|
+
nextStep: "replay_pending_turn_with_recreated_copilot_session",
|
|
837
|
+
...(input.turnIndex != null ? { iteration: input.turnIndex } : {}),
|
|
838
|
+
}, (failureMessage) => activityCtx.traceInfo(`[runTurn] ${failureMessage}`));
|
|
839
|
+
if (catalog) {
|
|
840
|
+
await cmsRetryBestEffort(`runTurn.recordEvent system.message-lossy-replay session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
841
|
+
eventType: "system.message",
|
|
842
|
+
data: {
|
|
843
|
+
content: "The runtime is replaying this turn after a worker restart lost the live Copilot session state before durable dehydrate completed. " +
|
|
844
|
+
"Some recent work may be missing or partially executed.",
|
|
845
|
+
},
|
|
846
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
850
|
+
turnIndex: 0,
|
|
851
|
+
trace,
|
|
852
|
+
lockHeld: true,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
catch (recoveryErr) {
|
|
856
|
+
const recoveryMessage = recoveryErr?.message || String(recoveryErr);
|
|
857
|
+
const fatalMessage = isMissingSessionStateErrorMessage(recoveryMessage)
|
|
858
|
+
? buildUnrecoverableSessionLossMessage(input.sessionId, stripMissingSessionStatePrefix(recoveryMessage))
|
|
859
|
+
: buildUnrecoverableSessionLossMessage(input.sessionId, recoveryMessage);
|
|
860
|
+
trace(`session=${input.sessionId} lossy replay session recreation failed error=${fatalMessage}`);
|
|
861
|
+
return await failForMissingState(fatalMessage);
|
|
862
|
+
}
|
|
863
|
+
effectivePrompt = mergePromptSections([LOSSY_SESSION_REPLAY_NOTICE, input.prompt]) || input.prompt;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
867
869
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
+
// ── Lifecycle p3: sentinel marks the dir as mid-mutation from here
|
|
871
|
+
// until the post-turn commit clears it. Written after getOrCreate so
|
|
872
|
+
// the turn-0 reset path cannot wipe it, before any body mutation.
|
|
873
|
+
if (lifecycle) {
|
|
874
|
+
writeTurnSentinel(path.join(sessionManager.getSessionStateDir(), input.sessionId), input.snapshot.turnKey);
|
|
875
|
+
if (lifecycleRehydrated) {
|
|
876
|
+
effectivePrompt = mergePromptSections([REHYDRATED_SESSION_NOTICE, effectivePrompt]) || effectivePrompt;
|
|
877
|
+
}
|
|
870
878
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
879
|
+
const getInlineClient = async () => {
|
|
880
|
+
if (inlineSdkClient)
|
|
881
|
+
return inlineSdkClient;
|
|
882
|
+
if (inlineSdkClientPromise)
|
|
883
|
+
return await inlineSdkClientPromise;
|
|
884
|
+
if (!storeUrl)
|
|
885
|
+
throw new Error("No storeUrl — cannot create PilotSwarmClient");
|
|
886
|
+
inlineSdkClientPromise = (async () => {
|
|
887
|
+
const startedClient = new PilotSwarmClient(internalClientConfig());
|
|
888
|
+
await startedClient.start();
|
|
889
|
+
inlineSdkClient = startedClient;
|
|
890
|
+
return startedClient;
|
|
891
|
+
})();
|
|
875
892
|
try {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
893
|
+
return await inlineSdkClientPromise;
|
|
894
|
+
}
|
|
895
|
+
finally {
|
|
896
|
+
inlineSdkClientPromise = null;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
const isAutonomousSystemTurn = () => {
|
|
900
|
+
if (!SYSTEM_AGENT_IDS.has(runConfig.agentIdentity || ""))
|
|
901
|
+
return false;
|
|
902
|
+
if (input.bootstrap === true)
|
|
903
|
+
return true;
|
|
904
|
+
return /^\s*\[SYSTEM:/i.test(String(input.prompt ?? ""));
|
|
905
|
+
};
|
|
906
|
+
const sanitizeAutonomousSystemSessionFilters = (args) => {
|
|
907
|
+
if (!args || !isAutonomousSystemTurn())
|
|
908
|
+
return args;
|
|
909
|
+
if (!args.owner_query && !args.owner_kind)
|
|
910
|
+
return args;
|
|
911
|
+
const sanitized = { ...args };
|
|
912
|
+
delete sanitized.owner_query;
|
|
913
|
+
delete sanitized.owner_kind;
|
|
914
|
+
return sanitized;
|
|
915
|
+
};
|
|
916
|
+
const isReadOnlyTuner = () => runConfig.agentIdentity === "agent-tuner";
|
|
917
|
+
const normalizeAgentLookup = (value) => (value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
918
|
+
const resolveAgentConfigInline = (agentName) => {
|
|
919
|
+
const agents = [
|
|
920
|
+
...(userAgents ?? []).map(a => ({ ...a, system: false, creatable: true })),
|
|
921
|
+
...(systemAgents ?? []).map(a => ({ ...a, creatable: false })),
|
|
922
|
+
];
|
|
923
|
+
let lookupNamespace;
|
|
924
|
+
let rawName = agentName;
|
|
925
|
+
if (agentName.includes(":")) {
|
|
926
|
+
const parts = agentName.split(":");
|
|
927
|
+
lookupNamespace = parts[0];
|
|
928
|
+
rawName = parts.slice(1).join(":");
|
|
929
|
+
}
|
|
930
|
+
const lookup = normalizeAgentLookup(rawName);
|
|
931
|
+
const lookupBase = lookup.replace(/agent$/, "");
|
|
932
|
+
return agents.find(a => {
|
|
933
|
+
if (lookupNamespace && normalizeAgentLookup(a.namespace) !== normalizeAgentLookup(lookupNamespace))
|
|
934
|
+
return false;
|
|
935
|
+
const candidates = [a.name, a.id, a.title].map(normalizeAgentLookup).filter(Boolean);
|
|
936
|
+
return candidates.includes(lookup) || (lookupBase && candidates.includes(lookupBase));
|
|
937
|
+
}) ?? null;
|
|
938
|
+
};
|
|
939
|
+
const loadDirectChildSessions = async () => {
|
|
940
|
+
const sdkClient = await getInlineClient();
|
|
941
|
+
const sessions = await sdkClient.listSessions();
|
|
942
|
+
const directChildren = sessions.filter(s => s.parentSessionId === input.sessionId);
|
|
943
|
+
return await Promise.all(directChildren.map(async (child) => {
|
|
944
|
+
const info = await sdkClient._getSessionInfo(child.sessionId);
|
|
945
|
+
const outcome = catalog ? await catalog.getChildOutcome(child.sessionId).catch(() => null) : null;
|
|
946
|
+
const outcomeResult = normalizeJsonObject(outcome?.resultJson?.current);
|
|
947
|
+
const contractCurrent = normalizeJsonObject(outcome?.contractJson?.current);
|
|
948
|
+
return {
|
|
949
|
+
orchId: `session-${child.sessionId}`,
|
|
950
|
+
sessionId: child.sessionId,
|
|
951
|
+
title: info.title ?? child.title,
|
|
952
|
+
status: info.status,
|
|
953
|
+
iterations: info.iterations ?? child.iterations ?? 0,
|
|
954
|
+
parentSessionId: child.parentSessionId,
|
|
955
|
+
isSystem: child.isSystem ?? info.isSystem ?? false,
|
|
956
|
+
agentId: child.agentId ?? info.agentId,
|
|
957
|
+
result: outcome?.summary ?? (typeof outcomeResult?.summary === "string" ? outcomeResult.summary : info.result),
|
|
958
|
+
contract: contractCurrent ?? undefined,
|
|
959
|
+
contractStatus: outcome?.contractJson ? "contracted" : undefined,
|
|
960
|
+
verdict: outcome?.verdict ?? undefined,
|
|
961
|
+
contractViolations: Array.isArray(outcomeResult?.contractViolations) ? outcomeResult.contractViolations : undefined,
|
|
962
|
+
error: info.error,
|
|
923
963
|
};
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
964
|
+
}));
|
|
965
|
+
};
|
|
966
|
+
const resolveManagedChild = async (agentId) => {
|
|
967
|
+
const targetOrchId = agentId.startsWith("session-") ? agentId : `session-${agentId}`;
|
|
968
|
+
const children = await loadDirectChildSessions();
|
|
969
|
+
const child = children.find(entry => entry.orchId === targetOrchId);
|
|
970
|
+
if (!child) {
|
|
971
|
+
throw new Error(`agent "${targetOrchId}" not found. Known agents: ${children.filter(entry => !entry.isSystem).map(entry => entry.orchId).join(", ") || "none"}`);
|
|
972
|
+
}
|
|
973
|
+
if (child.isSystem) {
|
|
974
|
+
throw new Error(`agent "${targetOrchId}" is a worker-managed system agent and is not a controllable spawned sub-agent`);
|
|
975
|
+
}
|
|
976
|
+
return child;
|
|
977
|
+
};
|
|
978
|
+
const controlToolBridge = {
|
|
979
|
+
spawnAgent: async (args) => {
|
|
980
|
+
try {
|
|
981
|
+
const childNestingLevel = (input.nestingLevel ?? 0) + 1;
|
|
982
|
+
if (childNestingLevel > MAX_NESTING_LEVEL) {
|
|
983
|
+
return `[SYSTEM: spawn_agent failed — you are already at nesting level ${input.nestingLevel ?? 0} (max ${MAX_NESTING_LEVEL}). ` +
|
|
984
|
+
`Sub-agents at this depth cannot spawn further sub-agents. Handle the task directly instead.]`;
|
|
985
|
+
}
|
|
986
|
+
const existingChildren = (await loadDirectChildSessions()).filter(child => !child.isSystem);
|
|
987
|
+
const activeCount = existingChildren.filter(child => child.status === "running").length;
|
|
988
|
+
if (activeCount >= MAX_SUB_AGENTS) {
|
|
989
|
+
return `[SYSTEM: spawn_agent failed — you already have ${activeCount} running sub-agents (max ${MAX_SUB_AGENTS}). ` +
|
|
990
|
+
`Wait for some to complete before spawning more.]`;
|
|
991
|
+
}
|
|
992
|
+
let agentTask = args.task || "";
|
|
993
|
+
let agentSystemMessage = args.system_message;
|
|
994
|
+
let agentToolNames = args.tool_names;
|
|
995
|
+
let agentModel = args.model;
|
|
996
|
+
let agentReasoningEffort = args.reasoning_effort;
|
|
997
|
+
let agentIsSystem = false;
|
|
998
|
+
const explicitAgentTitle = typeof args.title === "string" && args.title.trim() ? args.title.trim() : undefined;
|
|
999
|
+
let agentTitle = explicitAgentTitle;
|
|
1000
|
+
let agentTitleIsExplicit = Boolean(explicitAgentTitle);
|
|
1001
|
+
let agentId;
|
|
1002
|
+
let agentSplash;
|
|
1003
|
+
let agentSplashMobile;
|
|
1004
|
+
let boundAgentName;
|
|
1005
|
+
let promptLayeringKind;
|
|
1006
|
+
let resolvedAgentName = args.agent_name;
|
|
1007
|
+
const applyAgentDef = (agentDef, useDefinitionDefaults = false) => {
|
|
1008
|
+
agentTask = useDefinitionDefaults
|
|
1009
|
+
? (agentDef.initialPrompt || `You are the ${agentDef.name} agent. Begin your work.`)
|
|
1010
|
+
: (args.task || agentDef.initialPrompt || `You are the ${agentDef.name} agent. Begin your work.`);
|
|
1011
|
+
agentSystemMessage = useDefinitionDefaults ? undefined : args.system_message;
|
|
1012
|
+
agentToolNames = useDefinitionDefaults
|
|
1013
|
+
? (agentDef.tools ?? undefined)
|
|
1014
|
+
: (args.tool_names ?? agentDef.tools ?? undefined);
|
|
1015
|
+
agentIsSystem = agentDef.system ?? false;
|
|
1016
|
+
if (!agentTitleIsExplicit)
|
|
1017
|
+
agentTitle = agentDef.title;
|
|
1018
|
+
agentId = agentDef.id ?? resolvedAgentName;
|
|
1019
|
+
agentSplash = agentDef.splash;
|
|
1020
|
+
agentSplashMobile = agentDef.splashMobile;
|
|
1021
|
+
boundAgentName = agentDef.name;
|
|
1022
|
+
promptLayeringKind = agentDef.promptLayerKind
|
|
1023
|
+
?? (agentDef.system
|
|
1024
|
+
? ((agentDef.namespace || "pilotswarm") === "pilotswarm"
|
|
1025
|
+
? "pilotswarm-system-agent"
|
|
1026
|
+
: "app-system-agent")
|
|
1027
|
+
: "app-agent");
|
|
1028
|
+
};
|
|
1029
|
+
if (resolvedAgentName) {
|
|
1030
|
+
const agentDef = resolveAgentConfigInline(resolvedAgentName);
|
|
1031
|
+
if (!agentDef) {
|
|
1032
|
+
return `[SYSTEM: spawn_agent failed — agent "${resolvedAgentName}" not found. Use ps_list_agents to see available agents.]`;
|
|
1033
|
+
}
|
|
1034
|
+
if (agentDef.system && agentDef.creatable === false) {
|
|
1035
|
+
return `[SYSTEM: spawn_agent failed — agent "${resolvedAgentName}" is a worker-managed system agent and cannot be spawned from a session. ` +
|
|
1036
|
+
`If it is missing, the workers likely need to be restarted.]`;
|
|
1037
|
+
}
|
|
1038
|
+
applyAgentDef(agentDef, resolvedAgentName !== args.agent_name);
|
|
928
1039
|
}
|
|
929
|
-
if (
|
|
930
|
-
return `[SYSTEM: spawn_agent failed —
|
|
931
|
-
`
|
|
1040
|
+
if (agentModel && !agentModel.includes(":")) {
|
|
1041
|
+
return `[SYSTEM: spawn_agent failed — model "${agentModel}" is not allowed. ` +
|
|
1042
|
+
`When overriding a sub-agent model, first call list_available_models and then use the exact provider:model value from that list. ` +
|
|
1043
|
+
`If you are unsure, omit model so the sub-agent inherits your current model.]`;
|
|
932
1044
|
}
|
|
933
|
-
|
|
1045
|
+
// v1.0.49: same-name duplicate spawns are allowed. The
|
|
1046
|
+
// global MAX_SUB_AGENTS cap and per-spawn nesting limit
|
|
1047
|
+
// still apply. The parent is responsible for closing
|
|
1048
|
+
// each instance with complete_agent / cancel_agent /
|
|
1049
|
+
// delete_agent when it no longer needs the child.
|
|
1050
|
+
const { boundAgentName: _parentBoundAgentName, promptLayering: _parentPromptLayering, isCrawler: _parentIsCrawler, isHarvester: _parentIsHarvester, ...parentConfig } = input.config;
|
|
1051
|
+
const childConfig = {
|
|
1052
|
+
...parentConfig,
|
|
1053
|
+
...(agentModel ? { model: agentModel } : {}),
|
|
1054
|
+
...(agentReasoningEffort ? { reasoningEffort: agentReasoningEffort } : {}),
|
|
1055
|
+
...(agentSystemMessage ? { systemMessage: agentSystemMessage } : {}),
|
|
1056
|
+
...(boundAgentName ? { boundAgentName } : {}),
|
|
1057
|
+
...(promptLayeringKind ? { promptLayering: { kind: promptLayeringKind } } : {}),
|
|
1058
|
+
...(agentToolNames ? { toolNames: agentToolNames } : {}),
|
|
1059
|
+
...(args.contract ? { childContract: args.contract } : {}),
|
|
1060
|
+
};
|
|
1061
|
+
const parentSystemMsg = typeof childConfig.systemMessage === "string"
|
|
1062
|
+
? childConfig.systemMessage
|
|
1063
|
+
: childConfig.systemMessage?.content ?? "";
|
|
1064
|
+
const canSpawnMore = childNestingLevel < MAX_NESTING_LEVEL;
|
|
1065
|
+
const subAgentPreamble = `[SUB-AGENT CONTEXT]\n` +
|
|
1066
|
+
`You are a sub-agent spawned by a parent session (ID: session-${input.sessionId}).\n` +
|
|
1067
|
+
`Your nesting level: ${childNestingLevel} (max: ${MAX_NESTING_LEVEL}).\n` +
|
|
1068
|
+
`Your task: "${agentTask.slice(0, 500)}"\n\n` +
|
|
1069
|
+
`Instructions:\n` +
|
|
1070
|
+
`- Focus exclusively on your assigned task.\n` +
|
|
1071
|
+
`- Your final response will be automatically forwarded to the parent agent.\n` +
|
|
1072
|
+
`- Be thorough but concise — the parent will synthesize results from multiple agents.\n` +
|
|
1073
|
+
`- Do NOT ask the user for input — you are autonomous.\n` +
|
|
1074
|
+
`- You are autonomous and goal-driven. If the task implies ongoing monitoring or follow-through until done, keep yourself alive with durable timers until the goal is complete or you can no longer make progress.\n` +
|
|
1075
|
+
`- If it is ambiguous whether the task should become a long-running recurring workflow, report that ambiguity back to the parent instead of guessing or asking the user directly.\n` +
|
|
1076
|
+
`- When your task is complete, provide a clear summary of your findings/results.\n` +
|
|
1077
|
+
`- Prefer using \`store_fact\` for larger structured context handoffs across your session lineage. Put the durable details in facts, then pass fact keys or \`read_facts\` pointers in messages/prompts instead of pasting large context blobs.\n` +
|
|
1078
|
+
`- If you write any files with write_artifact, you MUST also call export_artifact and include the artifact:// link in your response.\n` +
|
|
1079
|
+
`- If you override a sub-agent model, you MUST first call list_available_models in this session and use only an exact provider:model value returned there. ` +
|
|
1080
|
+
`NEVER invent, guess, shorten, or reuse a stale model name.\n` +
|
|
1081
|
+
`- Worker-managed system agents are not valid spawn targets. If you expect one and it is missing, report that the workers likely need to be restarted.\n` +
|
|
1082
|
+
`- For ANY waiting, sleeping, delaying, or scheduling, you MUST use the \`wait\`, \`wait_on_worker\`, \`cron\`, or \`cron_at\` tools. ` +
|
|
1083
|
+
`Use \`wait\` or \`wait_on_worker\` for one-shot delays. Use \`cron\` for fixed recurring intervals and \`cron_at\` for wall-clock schedules. ` +
|
|
1084
|
+
`Do NOT implement wall-clock schedules by waking every N minutes to check the clock; use \`cron_at\` with an explicit IANA timezone instead. ` +
|
|
1085
|
+
`Do NOT burn tokens polling inside one LLM turn; after a brief immediate re-check at most, yield with a durable timer. ` +
|
|
1086
|
+
`NEVER use setTimeout, sleep, setInterval, or any other timing mechanism.\n` +
|
|
1087
|
+
(canSpawnMore
|
|
1088
|
+
? `- If your parent task explicitly asks you to spawn sub-agents, delegate, fan out, or parallelize work, you SHOULD do so within runtime limits instead of collapsing the task into a direct answer. ` +
|
|
1089
|
+
`If delegation was not explicitly requested, use your judgment and avoid unnecessary fan-out. ` +
|
|
1090
|
+
`You have ${MAX_NESTING_LEVEL - childNestingLevel} level(s) of nesting remaining. After spawning, call wait_for_agents to block until they finish.\n`
|
|
1091
|
+
: `- You CANNOT spawn sub-agents — you are at the maximum nesting depth. Handle everything directly.\n`);
|
|
1092
|
+
childConfig.systemMessage = subAgentPreamble + (parentSystemMsg ? "\n\n" + parentSystemMsg : "");
|
|
1093
|
+
const sdkClient = await getInlineClient();
|
|
1094
|
+
const normalizedModel = sessionManager.normalizeModelRef(childConfig.model);
|
|
1095
|
+
if (normalizedModel)
|
|
1096
|
+
childConfig.model = normalizedModel;
|
|
1097
|
+
const childSession = await sdkClient.createSession({
|
|
1098
|
+
parentSessionId: input.sessionId,
|
|
1099
|
+
nestingLevel: childNestingLevel,
|
|
1100
|
+
model: childConfig.model,
|
|
1101
|
+
reasoningEffort: childConfig.reasoningEffort,
|
|
1102
|
+
systemMessage: childConfig.systemMessage,
|
|
1103
|
+
boundAgentName: childConfig.boundAgentName,
|
|
1104
|
+
promptLayering: childConfig.promptLayering,
|
|
1105
|
+
toolNames: childConfig.toolNames,
|
|
1106
|
+
waitThreshold: childConfig.waitThreshold,
|
|
1107
|
+
agentId,
|
|
1108
|
+
});
|
|
1109
|
+
if (catalog) {
|
|
1110
|
+
const meta = {};
|
|
1111
|
+
if (agentTitle) {
|
|
1112
|
+
meta.title = (agentTitleIsExplicit || agentIsSystem)
|
|
1113
|
+
? agentTitle
|
|
1114
|
+
: `${agentTitle}: ${childSession.sessionId.slice(0, 8)}`;
|
|
1115
|
+
}
|
|
1116
|
+
if (agentId)
|
|
1117
|
+
meta.agentId = agentId;
|
|
1118
|
+
if (agentSplash)
|
|
1119
|
+
meta.splash = agentSplash;
|
|
1120
|
+
if (agentSplashMobile)
|
|
1121
|
+
meta.splashMobile = agentSplashMobile;
|
|
1122
|
+
if (Object.keys(meta).length > 0) {
|
|
1123
|
+
// Best-effort: child has been created and is about to be
|
|
1124
|
+
// sent its bootstrap. A failed meta update means the row
|
|
1125
|
+
// is missing title/agentId/splash, not that the spawn
|
|
1126
|
+
// failed. Don't escalate to a thrown spawn_agent.
|
|
1127
|
+
const capturedMeta = meta;
|
|
1128
|
+
await cmsRetryBestEffort(`runTurn.spawn.updateSession meta session=${childSession.sessionId}`, () => catalog.updateSession(childSession.sessionId, capturedMeta), (msg) => activityCtx.traceInfo(msg));
|
|
1129
|
+
}
|
|
1130
|
+
const contractJson = buildContractJson(args.contract, input.sessionId, childSession.sessionId);
|
|
1131
|
+
if (contractJson) {
|
|
1132
|
+
await cmsRetryCritical(`runTurn.spawn.upsertChildOutcome contract child=${childSession.sessionId}`, () => catalog.upsertChildOutcome({
|
|
1133
|
+
childSessionId: childSession.sessionId,
|
|
1134
|
+
parentSessionId: input.sessionId,
|
|
1135
|
+
contractJson,
|
|
1136
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
await childSession.send(agentTask, { bootstrap: true });
|
|
1140
|
+
if (catalog) {
|
|
1141
|
+
await cmsRetryBestEffort(`runTurn.spawn.recordEvent agent_spawned session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1142
|
+
eventType: "session.agent_spawned",
|
|
1143
|
+
data: { childSessionId: childSession.sessionId, agentId: agentId || undefined, task: agentTask.slice(0, 500) },
|
|
1144
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1145
|
+
}
|
|
1146
|
+
const childOrchId = `session-${childSession.sessionId}`;
|
|
1147
|
+
return `[SYSTEM: Sub-agent spawned successfully.\n` +
|
|
1148
|
+
` Agent ID: ${childOrchId}\n` +
|
|
1149
|
+
` ${resolvedAgentName ? `Agent: ${resolvedAgentName}\n ` : ``}Task: "${agentTask.slice(0, 200)}"\n` +
|
|
1150
|
+
` The agent is now running autonomously. Continue your work in this SAME turn and keep following the user's remaining steps. ` +
|
|
1151
|
+
`Do NOT stop just because the child started. If your plan says to pause, call wait or wait_for_agents explicitly. ` +
|
|
1152
|
+
`You can also use check_agents to poll status, ` +
|
|
1153
|
+
`or message_agent to send instructions.]`;
|
|
934
1154
|
}
|
|
935
|
-
|
|
936
|
-
return `[SYSTEM: spawn_agent failed
|
|
937
|
-
`When overriding a sub-agent model, first call list_available_models and then use the exact provider:model value from that list. ` +
|
|
938
|
-
`If you are unsure, omit model so the sub-agent inherits your current model.]`;
|
|
1155
|
+
catch (err) {
|
|
1156
|
+
return `[SYSTEM: spawn_agent failed: ${err?.message || String(err)}]`;
|
|
939
1157
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
`Your nesting level: ${childNestingLevel} (max: ${MAX_NESTING_LEVEL}).\n` +
|
|
963
|
-
`Your task: "${agentTask.slice(0, 500)}"\n\n` +
|
|
964
|
-
`Instructions:\n` +
|
|
965
|
-
`- Focus exclusively on your assigned task.\n` +
|
|
966
|
-
`- Your final response will be automatically forwarded to the parent agent.\n` +
|
|
967
|
-
`- Be thorough but concise — the parent will synthesize results from multiple agents.\n` +
|
|
968
|
-
`- Do NOT ask the user for input — you are autonomous.\n` +
|
|
969
|
-
`- You are autonomous and goal-driven. If the task implies ongoing monitoring or follow-through until done, keep yourself alive with durable timers until the goal is complete or you can no longer make progress.\n` +
|
|
970
|
-
`- If it is ambiguous whether the task should become a long-running recurring workflow, report that ambiguity back to the parent instead of guessing or asking the user directly.\n` +
|
|
971
|
-
`- When your task is complete, provide a clear summary of your findings/results.\n` +
|
|
972
|
-
`- Prefer using \`store_fact\` for larger structured context handoffs across your session lineage. Put the durable details in facts, then pass fact keys or \`read_facts\` pointers in messages/prompts instead of pasting large context blobs.\n` +
|
|
973
|
-
`- If you write any files with write_artifact, you MUST also call export_artifact and include the artifact:// link in your response.\n` +
|
|
974
|
-
`- If you override a sub-agent model, you MUST first call list_available_models in this session and use only an exact provider:model value returned there. ` +
|
|
975
|
-
`NEVER invent, guess, shorten, or reuse a stale model name.\n` +
|
|
976
|
-
`- Worker-managed system agents are not valid spawn targets. If you expect one and it is missing, report that the workers likely need to be restarted.\n` +
|
|
977
|
-
`- For ANY waiting, sleeping, delaying, or scheduling, you MUST use the \`wait\`, \`wait_on_worker\`, \`cron\`, or \`cron_at\` tools. ` +
|
|
978
|
-
`Use \`wait\` or \`wait_on_worker\` for one-shot delays. Use \`cron\` for fixed recurring intervals and \`cron_at\` for wall-clock schedules. ` +
|
|
979
|
-
`Do NOT implement wall-clock schedules by waking every N minutes to check the clock; use \`cron_at\` with an explicit IANA timezone instead. ` +
|
|
980
|
-
`Do NOT burn tokens polling inside one LLM turn; after a brief immediate re-check at most, yield with a durable timer. ` +
|
|
981
|
-
`NEVER use setTimeout, sleep, setInterval, or any other timing mechanism.\n` +
|
|
982
|
-
(canSpawnMore
|
|
983
|
-
? `- If your parent task explicitly asks you to spawn sub-agents, delegate, fan out, or parallelize work, you SHOULD do so within runtime limits instead of collapsing the task into a direct answer. ` +
|
|
984
|
-
`If delegation was not explicitly requested, use your judgment and avoid unnecessary fan-out. ` +
|
|
985
|
-
`You have ${MAX_NESTING_LEVEL - childNestingLevel} level(s) of nesting remaining. After spawning, call wait_for_agents to block until they finish.\n`
|
|
986
|
-
: `- You CANNOT spawn sub-agents — you are at the maximum nesting depth. Handle everything directly.\n`);
|
|
987
|
-
childConfig.systemMessage = subAgentPreamble + (parentSystemMsg ? "\n\n" + parentSystemMsg : "");
|
|
988
|
-
const sdkClient = await getInlineClient();
|
|
989
|
-
const normalizedModel = sessionManager.normalizeModelRef(childConfig.model);
|
|
990
|
-
if (normalizedModel)
|
|
991
|
-
childConfig.model = normalizedModel;
|
|
992
|
-
const childSession = await sdkClient.createSession({
|
|
993
|
-
parentSessionId: input.sessionId,
|
|
994
|
-
nestingLevel: childNestingLevel,
|
|
995
|
-
model: childConfig.model,
|
|
996
|
-
reasoningEffort: childConfig.reasoningEffort,
|
|
997
|
-
systemMessage: childConfig.systemMessage,
|
|
998
|
-
boundAgentName: childConfig.boundAgentName,
|
|
999
|
-
promptLayering: childConfig.promptLayering,
|
|
1000
|
-
toolNames: childConfig.toolNames,
|
|
1001
|
-
waitThreshold: childConfig.waitThreshold,
|
|
1002
|
-
agentId,
|
|
1003
|
-
});
|
|
1004
|
-
if (catalog) {
|
|
1005
|
-
const meta = {};
|
|
1006
|
-
if (agentTitle) {
|
|
1007
|
-
meta.title = (agentTitleIsExplicit || agentIsSystem)
|
|
1008
|
-
? agentTitle
|
|
1009
|
-
: `${agentTitle}: ${childSession.sessionId.slice(0, 8)}`;
|
|
1010
|
-
}
|
|
1011
|
-
if (agentId)
|
|
1012
|
-
meta.agentId = agentId;
|
|
1013
|
-
if (agentSplash)
|
|
1014
|
-
meta.splash = agentSplash;
|
|
1015
|
-
if (agentSplashMobile)
|
|
1016
|
-
meta.splashMobile = agentSplashMobile;
|
|
1017
|
-
if (Object.keys(meta).length > 0) {
|
|
1018
|
-
// Best-effort: child has been created and is about to be
|
|
1019
|
-
// sent its bootstrap. A failed meta update means the row
|
|
1020
|
-
// is missing title/agentId/splash, not that the spawn
|
|
1021
|
-
// failed. Don't escalate to a thrown spawn_agent.
|
|
1022
|
-
const capturedMeta = meta;
|
|
1023
|
-
await cmsRetryBestEffort(`runTurn.spawn.updateSession meta session=${childSession.sessionId}`, () => catalog.updateSession(childSession.sessionId, capturedMeta), (msg) => activityCtx.traceInfo(msg));
|
|
1158
|
+
},
|
|
1159
|
+
setSessionModel: async (args) => {
|
|
1160
|
+
try {
|
|
1161
|
+
const requestedModel = String(args.model || "").trim();
|
|
1162
|
+
if (!requestedModel)
|
|
1163
|
+
return "[SYSTEM: set_session_model failed: model is required.]";
|
|
1164
|
+
if (!storeUrl)
|
|
1165
|
+
return "[SYSTEM: set_session_model failed: no storeUrl is configured.]";
|
|
1166
|
+
const resolved = sessionManager.resolveModelSwitchConfig(requestedModel, "reasoning_effort" in args ? args.reasoning_effort ?? null : undefined);
|
|
1167
|
+
const sdkClient = new PilotSwarmClient(internalClientConfig());
|
|
1168
|
+
try {
|
|
1169
|
+
await sdkClient.start();
|
|
1170
|
+
await sdkClient.duroxideClient.enqueueEvent(`session-${input.sessionId}`, "messages", JSON.stringify({
|
|
1171
|
+
type: "cmd",
|
|
1172
|
+
cmd: "set_model",
|
|
1173
|
+
id: `set-model-tool-${randomUUID()}`,
|
|
1174
|
+
args: {
|
|
1175
|
+
model: resolved.model,
|
|
1176
|
+
reasoningEffort: resolved.reasoningEffort,
|
|
1177
|
+
source: "tool",
|
|
1178
|
+
},
|
|
1179
|
+
}));
|
|
1024
1180
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
await cmsRetryCritical(`runTurn.spawn.upsertChildOutcome contract child=${childSession.sessionId}`, () => catalog.upsertChildOutcome({
|
|
1028
|
-
childSessionId: childSession.sessionId,
|
|
1029
|
-
parentSessionId: input.sessionId,
|
|
1030
|
-
contractJson,
|
|
1031
|
-
}), (msg) => activityCtx.traceInfo(msg));
|
|
1181
|
+
finally {
|
|
1182
|
+
await sdkClient.stop().catch(() => { });
|
|
1032
1183
|
}
|
|
1184
|
+
const effortText = resolved.reasoningEffort ? `:${resolved.reasoningEffort}` : "";
|
|
1185
|
+
return `[SYSTEM: Model switch accepted. Stop this turn now; the runtime will automatically continue on ${resolved.model}${effortText}.]`;
|
|
1033
1186
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
await cmsRetryBestEffort(`runTurn.spawn.recordEvent agent_spawned session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1037
|
-
eventType: "session.agent_spawned",
|
|
1038
|
-
data: { childSessionId: childSession.sessionId, agentId: agentId || undefined, task: agentTask.slice(0, 500) },
|
|
1039
|
-
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
return `[SYSTEM: set_session_model failed: ${err?.message || String(err)}]`;
|
|
1040
1189
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
` Agent ID: ${childOrchId}\n` +
|
|
1044
|
-
` ${resolvedAgentName ? `Agent: ${resolvedAgentName}\n ` : ``}Task: "${agentTask.slice(0, 200)}"\n` +
|
|
1045
|
-
` The agent is now running autonomously. Continue your work in this SAME turn and keep following the user's remaining steps. ` +
|
|
1046
|
-
`Do NOT stop just because the child started. If your plan says to pause, call wait or wait_for_agents explicitly. ` +
|
|
1047
|
-
`You can also use check_agents to poll status, ` +
|
|
1048
|
-
`or message_agent to send instructions.]`;
|
|
1049
|
-
}
|
|
1050
|
-
catch (err) {
|
|
1051
|
-
return `[SYSTEM: spawn_agent failed: ${err?.message || String(err)}]`;
|
|
1052
|
-
}
|
|
1053
|
-
},
|
|
1054
|
-
setSessionModel: async (args) => {
|
|
1055
|
-
try {
|
|
1056
|
-
const requestedModel = String(args.model || "").trim();
|
|
1057
|
-
if (!requestedModel)
|
|
1058
|
-
return "[SYSTEM: set_session_model failed: model is required.]";
|
|
1059
|
-
if (!storeUrl)
|
|
1060
|
-
return "[SYSTEM: set_session_model failed: no storeUrl is configured.]";
|
|
1061
|
-
const resolved = sessionManager.resolveModelSwitchConfig(requestedModel, "reasoning_effort" in args ? args.reasoning_effort ?? null : undefined);
|
|
1062
|
-
const sdkClient = new PilotSwarmClient(internalClientConfig());
|
|
1190
|
+
},
|
|
1191
|
+
messageAgent: async (args) => {
|
|
1063
1192
|
try {
|
|
1064
|
-
await
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1193
|
+
const child = await resolveManagedChild(args.agent_id);
|
|
1194
|
+
if (catalog && args.contract_patch) {
|
|
1195
|
+
const existing = await catalog.getChildOutcome(child.sessionId);
|
|
1196
|
+
const contractJson = appendContractPatchJson(existing?.contractJson ?? null, args.contract_patch, input.sessionId, child.sessionId);
|
|
1197
|
+
if (contractJson) {
|
|
1198
|
+
await cmsRetryCritical(`runTurn.message.upsertChildOutcome contract child=${child.sessionId}`, () => catalog.upsertChildOutcome({
|
|
1199
|
+
childSessionId: child.sessionId,
|
|
1200
|
+
parentSessionId: input.sessionId,
|
|
1201
|
+
contractJson,
|
|
1202
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const sdkClient = await getInlineClient();
|
|
1206
|
+
await sdkClient._getDuroxideClient().enqueueEvent(child.orchId, "messages", JSON.stringify({ prompt: args.message }));
|
|
1207
|
+
return `[SYSTEM: Message sent to sub-agent ${child.orchId}: "${args.message.slice(0, 200)}". ` +
|
|
1208
|
+
`Continue your work in this SAME turn. If you are waiting on the child, call wait_for_agents explicitly rather than stopping here.]`;
|
|
1075
1209
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1210
|
+
catch (err) {
|
|
1211
|
+
return `[SYSTEM: message_agent failed: ${err?.message || String(err)}]`;
|
|
1078
1212
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
},
|
|
1086
|
-
messageAgent: async (args) => {
|
|
1087
|
-
try {
|
|
1088
|
-
const child = await resolveManagedChild(args.agent_id);
|
|
1089
|
-
if (catalog && args.contract_patch) {
|
|
1090
|
-
const existing = await catalog.getChildOutcome(child.sessionId);
|
|
1091
|
-
const contractJson = appendContractPatchJson(existing?.contractJson ?? null, args.contract_patch, input.sessionId, child.sessionId);
|
|
1092
|
-
if (contractJson) {
|
|
1093
|
-
await cmsRetryCritical(`runTurn.message.upsertChildOutcome contract child=${child.sessionId}`, () => catalog.upsertChildOutcome({
|
|
1094
|
-
childSessionId: child.sessionId,
|
|
1095
|
-
parentSessionId: input.sessionId,
|
|
1096
|
-
contractJson,
|
|
1097
|
-
}), (msg) => activityCtx.traceInfo(msg));
|
|
1213
|
+
},
|
|
1214
|
+
checkAgents: async () => {
|
|
1215
|
+
try {
|
|
1216
|
+
const children = (await loadDirectChildSessions()).filter(child => !child.isSystem);
|
|
1217
|
+
if (children.length === 0) {
|
|
1218
|
+
return `[SYSTEM: No sub-agents have been spawned yet.]`;
|
|
1098
1219
|
}
|
|
1220
|
+
const statusLines = children.map((agent) => ` - Agent ${agent.orchId}\n` +
|
|
1221
|
+
` Title: ${agent.title ?? "(untitled)"}\n` +
|
|
1222
|
+
` Status: ${agent.status}\n` +
|
|
1223
|
+
` Contract: ${agent.contractStatus ?? "none"}\n` +
|
|
1224
|
+
` Verdict: ${agent.verdict ?? "none"}\n` +
|
|
1225
|
+
` Iterations: ${agent.iterations ?? 0}\n` +
|
|
1226
|
+
(Array.isArray(agent.contractViolations) && agent.contractViolations.length > 0
|
|
1227
|
+
? ` Violations: ${agent.contractViolations.map((v) => v?.code || v?.message || "violation").join(", ")}\n`
|
|
1228
|
+
: "") +
|
|
1229
|
+
` Output: ${agent.result ?? agent.error ?? "(no output yet)"}`);
|
|
1230
|
+
return `[SYSTEM: Sub-agent status report (${children.length} agents):\n${statusLines.join("\n")}]`;
|
|
1099
1231
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
catch (err) {
|
|
1106
|
-
return `[SYSTEM: message_agent failed: ${err?.message || String(err)}]`;
|
|
1107
|
-
}
|
|
1108
|
-
},
|
|
1109
|
-
checkAgents: async () => {
|
|
1110
|
-
try {
|
|
1232
|
+
catch (err) {
|
|
1233
|
+
return `[SYSTEM: check_agents failed: ${err?.message || String(err)}]`;
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
resolveWaitForAgents: async (agentIds) => {
|
|
1111
1237
|
const children = (await loadDirectChildSessions()).filter(child => !child.isSystem);
|
|
1112
|
-
if (
|
|
1113
|
-
return
|
|
1238
|
+
if (agentIds && agentIds.length > 0) {
|
|
1239
|
+
return await Promise.all(agentIds.map(async (agentId) => (await resolveManagedChild(agentId)).orchId));
|
|
1114
1240
|
}
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
listSessions: async (args) => {
|
|
1140
|
-
try {
|
|
1141
|
-
const sdkClient = await getInlineClient();
|
|
1142
|
-
const effectiveArgs = sanitizeAutonomousSystemSessionFilters(args);
|
|
1143
|
-
const limit = Math.max(1, Math.min(100, Number(effectiveArgs?.limit) || 50));
|
|
1144
|
-
const query = String(effectiveArgs?.query || "").trim().toLowerCase();
|
|
1145
|
-
const exactSessionId = String(effectiveArgs?.session_id || "").replace(/^session-/, "");
|
|
1146
|
-
const agentId = String(effectiveArgs?.agent_id || "").trim();
|
|
1147
|
-
const state = String(effectiveArgs?.state || "").trim().toLowerCase();
|
|
1148
|
-
const parentSessionId = String(effectiveArgs?.parent_session_id || "").replace(/^session-/, "");
|
|
1149
|
-
const groupFilterRaw = effectiveArgs?.group_id;
|
|
1150
|
-
const groupFilter = typeof groupFilterRaw === "string" ? groupFilterRaw.trim() : undefined;
|
|
1151
|
-
const includeChildren = effectiveArgs?.include_children === true;
|
|
1152
|
-
const updatedSince = Date.parse(String(effectiveArgs?.updated_since || ""));
|
|
1153
|
-
const summaryUpdatedSince = Date.parse(String(effectiveArgs?.summary_updated_since || ""));
|
|
1154
|
-
const sessions = (await sdkClient.listSessions()).filter((session) => matchesSessionOwnerFilters(session, {
|
|
1155
|
-
includeSystem: effectiveArgs?.include_system === true,
|
|
1156
|
-
ownerQuery: effectiveArgs?.owner_query,
|
|
1157
|
-
ownerKind: effectiveArgs?.owner_kind,
|
|
1158
|
-
})).filter((session) => {
|
|
1159
|
-
if (!includeChildren && session.parentSessionId)
|
|
1160
|
-
return false;
|
|
1161
|
-
if (exactSessionId && session.sessionId !== exactSessionId)
|
|
1162
|
-
return false;
|
|
1163
|
-
if (agentId && session.agentId !== agentId)
|
|
1164
|
-
return false;
|
|
1165
|
-
if (state && String(session.status || "").toLowerCase() !== state)
|
|
1166
|
-
return false;
|
|
1167
|
-
if (parentSessionId && session.parentSessionId !== parentSessionId)
|
|
1168
|
-
return false;
|
|
1169
|
-
if (groupFilter === "null" && session.groupId)
|
|
1170
|
-
return false;
|
|
1171
|
-
if (groupFilter && groupFilter !== "null" && session.groupId !== groupFilter)
|
|
1172
|
-
return false;
|
|
1173
|
-
if (Number.isFinite(updatedSince)) {
|
|
1174
|
-
const updatedAt = Date.parse(session.updatedAt || session.lastActiveAt || session.createdAt || "");
|
|
1175
|
-
if (!Number.isFinite(updatedAt) || updatedAt < updatedSince)
|
|
1241
|
+
const running = children.filter(child => child.status === "running").map(child => child.orchId);
|
|
1242
|
+
return running.length > 0 ? running : children.map(child => child.orchId);
|
|
1243
|
+
},
|
|
1244
|
+
listSessions: async (args) => {
|
|
1245
|
+
try {
|
|
1246
|
+
const sdkClient = await getInlineClient();
|
|
1247
|
+
const effectiveArgs = sanitizeAutonomousSystemSessionFilters(args);
|
|
1248
|
+
const limit = Math.max(1, Math.min(100, Number(effectiveArgs?.limit) || 50));
|
|
1249
|
+
const query = String(effectiveArgs?.query || "").trim().toLowerCase();
|
|
1250
|
+
const exactSessionId = String(effectiveArgs?.session_id || "").replace(/^session-/, "");
|
|
1251
|
+
const agentId = String(effectiveArgs?.agent_id || "").trim();
|
|
1252
|
+
const state = String(effectiveArgs?.state || "").trim().toLowerCase();
|
|
1253
|
+
const parentSessionId = String(effectiveArgs?.parent_session_id || "").replace(/^session-/, "");
|
|
1254
|
+
const groupFilterRaw = effectiveArgs?.group_id;
|
|
1255
|
+
const groupFilter = typeof groupFilterRaw === "string" ? groupFilterRaw.trim() : undefined;
|
|
1256
|
+
const includeChildren = effectiveArgs?.include_children === true;
|
|
1257
|
+
const updatedSince = Date.parse(String(effectiveArgs?.updated_since || ""));
|
|
1258
|
+
const summaryUpdatedSince = Date.parse(String(effectiveArgs?.summary_updated_since || ""));
|
|
1259
|
+
const sessions = (await sdkClient.listSessions()).filter((session) => matchesSessionOwnerFilters(session, {
|
|
1260
|
+
includeSystem: effectiveArgs?.include_system === true,
|
|
1261
|
+
ownerQuery: effectiveArgs?.owner_query,
|
|
1262
|
+
ownerKind: effectiveArgs?.owner_kind,
|
|
1263
|
+
})).filter((session) => {
|
|
1264
|
+
if (!includeChildren && session.parentSessionId)
|
|
1176
1265
|
return false;
|
|
1177
|
-
|
|
1178
|
-
if (Number.isFinite(summaryUpdatedSince)) {
|
|
1179
|
-
const summaryUpdatedAt = Date.parse(session.summaryUpdatedAt || "");
|
|
1180
|
-
if (!Number.isFinite(summaryUpdatedAt) || summaryUpdatedAt < summaryUpdatedSince)
|
|
1266
|
+
if (exactSessionId && session.sessionId !== exactSessionId)
|
|
1181
1267
|
return false;
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
session.summaryState?.intent,
|
|
1190
|
-
session.summaryState?.summary,
|
|
1191
|
-
formatSessionOwnerLabel(session),
|
|
1192
|
-
].map((part) => String(part || "").toLowerCase()).join(" ");
|
|
1193
|
-
if (!haystack.includes(query))
|
|
1268
|
+
if (agentId && session.agentId !== agentId)
|
|
1269
|
+
return false;
|
|
1270
|
+
if (state && String(session.status || "").toLowerCase() !== state)
|
|
1271
|
+
return false;
|
|
1272
|
+
if (parentSessionId && session.parentSessionId !== parentSessionId)
|
|
1273
|
+
return false;
|
|
1274
|
+
if (groupFilter === "null" && session.groupId)
|
|
1194
1275
|
return false;
|
|
1276
|
+
if (groupFilter && groupFilter !== "null" && session.groupId !== groupFilter)
|
|
1277
|
+
return false;
|
|
1278
|
+
if (Number.isFinite(updatedSince)) {
|
|
1279
|
+
const updatedAt = Date.parse(session.updatedAt || session.lastActiveAt || session.createdAt || "");
|
|
1280
|
+
if (!Number.isFinite(updatedAt) || updatedAt < updatedSince)
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
if (Number.isFinite(summaryUpdatedSince)) {
|
|
1284
|
+
const summaryUpdatedAt = Date.parse(session.summaryUpdatedAt || "");
|
|
1285
|
+
if (!Number.isFinite(summaryUpdatedAt) || summaryUpdatedAt < summaryUpdatedSince)
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
if (query) {
|
|
1289
|
+
const haystack = [
|
|
1290
|
+
session.sessionId,
|
|
1291
|
+
session.title,
|
|
1292
|
+
session.agentId,
|
|
1293
|
+
session.shortSummary,
|
|
1294
|
+
session.summaryState?.intent,
|
|
1295
|
+
session.summaryState?.summary,
|
|
1296
|
+
formatSessionOwnerLabel(session),
|
|
1297
|
+
].map((part) => String(part || "").toLowerCase()).join(" ");
|
|
1298
|
+
if (!haystack.includes(query))
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
return true;
|
|
1302
|
+
}).slice(0, limit);
|
|
1303
|
+
if (sessions.length === 0) {
|
|
1304
|
+
return "[SYSTEM: Active sessions (0). No sessions matched the requested filters.]";
|
|
1195
1305
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1306
|
+
const lines = sessions.map((s) => ` - ${s.sessionId}${s.sessionId === input.sessionId ? " (this session)" : ""}\n` +
|
|
1307
|
+
` Title: ${s.title ?? "(untitled)"}\n` +
|
|
1308
|
+
` Owner: ${formatSessionOwnerLabel(s)}\n` +
|
|
1309
|
+
` Agent: ${s.agentId ?? "generic"}\n` +
|
|
1310
|
+
` Group: ${s.groupId ?? "none"}\n` +
|
|
1311
|
+
` Summary: ${s.shortSummary ?? s.summaryState?.summary ?? "(no summary)"}\n` +
|
|
1312
|
+
` Summary Updated: ${s.summaryUpdatedAt ? new Date(s.summaryUpdatedAt).toISOString() : "never"}\n` +
|
|
1313
|
+
` Status: ${s.status}, Iterations: ${s.iterations ?? 0}\n` +
|
|
1314
|
+
` Parent: ${s.parentSessionId ?? "none"}`);
|
|
1315
|
+
return `[SYSTEM: Active sessions (${sessions.length}):\n${lines.join("\n")}]`;
|
|
1200
1316
|
}
|
|
1201
|
-
|
|
1202
|
-
`
|
|
1203
|
-
` Owner: ${formatSessionOwnerLabel(s)}\n` +
|
|
1204
|
-
` Agent: ${s.agentId ?? "generic"}\n` +
|
|
1205
|
-
` Group: ${s.groupId ?? "none"}\n` +
|
|
1206
|
-
` Summary: ${s.shortSummary ?? s.summaryState?.summary ?? "(no summary)"}\n` +
|
|
1207
|
-
` Summary Updated: ${s.summaryUpdatedAt ? new Date(s.summaryUpdatedAt).toISOString() : "never"}\n` +
|
|
1208
|
-
` Status: ${s.status}, Iterations: ${s.iterations ?? 0}\n` +
|
|
1209
|
-
` Parent: ${s.parentSessionId ?? "none"}`);
|
|
1210
|
-
return `[SYSTEM: Active sessions (${sessions.length}):\n${lines.join("\n")}]`;
|
|
1211
|
-
}
|
|
1212
|
-
catch (err) {
|
|
1213
|
-
return `[SYSTEM: list_sessions failed: ${err?.message || String(err)}]`;
|
|
1214
|
-
}
|
|
1215
|
-
},
|
|
1216
|
-
updateSessionSummary: async (args) => {
|
|
1217
|
-
try {
|
|
1218
|
-
if (isReadOnlyTuner())
|
|
1219
|
-
return `[SYSTEM: update_session_summary is disabled for read-only agent-tuner sessions.]`;
|
|
1220
|
-
if (!catalog)
|
|
1221
|
-
return `[SYSTEM: update_session_summary failed: CMS catalog is unavailable.]`;
|
|
1222
|
-
const hasSummaryState = Object.prototype.hasOwnProperty.call(args ?? {}, "summary_state") && args.summary_state !== undefined && args.summary_state !== null;
|
|
1223
|
-
const hasTitle = Object.prototype.hasOwnProperty.call(args ?? {}, "title");
|
|
1224
|
-
if (!hasSummaryState && !hasTitle) {
|
|
1225
|
-
return `[SYSTEM: update_session_summary failed: summary_state or title is required.]`;
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
return `[SYSTEM: list_sessions failed: ${err?.message || String(err)}]`;
|
|
1226
1319
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
if (
|
|
1231
|
-
return `[SYSTEM: update_session_summary
|
|
1232
|
-
|
|
1320
|
+
},
|
|
1321
|
+
updateSessionSummary: async (args) => {
|
|
1322
|
+
try {
|
|
1323
|
+
if (isReadOnlyTuner())
|
|
1324
|
+
return `[SYSTEM: update_session_summary is disabled for read-only agent-tuner sessions.]`;
|
|
1325
|
+
if (!catalog)
|
|
1326
|
+
return `[SYSTEM: update_session_summary failed: CMS catalog is unavailable.]`;
|
|
1327
|
+
const hasSummaryState = Object.prototype.hasOwnProperty.call(args ?? {}, "summary_state") && args.summary_state !== undefined && args.summary_state !== null;
|
|
1328
|
+
const hasTitle = Object.prototype.hasOwnProperty.call(args ?? {}, "title");
|
|
1329
|
+
if (!hasSummaryState && !hasTitle) {
|
|
1330
|
+
return `[SYSTEM: update_session_summary failed: summary_state or title is required.]`;
|
|
1331
|
+
}
|
|
1332
|
+
let normalizedSummary;
|
|
1333
|
+
if (hasSummaryState) {
|
|
1334
|
+
const summaryResult = normalizeSummaryState(args.summary_state);
|
|
1335
|
+
if (!summaryResult.ok)
|
|
1336
|
+
return `[SYSTEM: update_session_summary failed: ${summaryResult.message}]`;
|
|
1337
|
+
normalizedSummary = summaryResult;
|
|
1338
|
+
}
|
|
1339
|
+
const titleUpdate = hasTitle
|
|
1340
|
+
? await prepareStickySessionTitleUpdate(catalog, input.sessionId, args.title)
|
|
1341
|
+
: undefined;
|
|
1342
|
+
if (titleUpdate && !titleUpdate.ok)
|
|
1343
|
+
return `[SYSTEM: update_session_summary failed: ${titleUpdate.message}]`;
|
|
1344
|
+
const updated = [];
|
|
1345
|
+
if (normalizedSummary) {
|
|
1346
|
+
await catalog.updateSessionSummary(input.sessionId, normalizedSummary.summaryState, args.short_summary);
|
|
1347
|
+
updated.push("summary");
|
|
1348
|
+
}
|
|
1349
|
+
if (titleUpdate?.ok) {
|
|
1350
|
+
await catalog.updateSession(input.sessionId, titleUpdate.updates);
|
|
1351
|
+
updated.push("title");
|
|
1352
|
+
}
|
|
1353
|
+
return `[SYSTEM: Session ${updated.join(" and ")} updated.]`;
|
|
1233
1354
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
: undefined;
|
|
1237
|
-
if (titleUpdate && !titleUpdate.ok)
|
|
1238
|
-
return `[SYSTEM: update_session_summary failed: ${titleUpdate.message}]`;
|
|
1239
|
-
const updated = [];
|
|
1240
|
-
if (normalizedSummary) {
|
|
1241
|
-
await catalog.updateSessionSummary(input.sessionId, normalizedSummary.summaryState, args.short_summary);
|
|
1242
|
-
updated.push("summary");
|
|
1355
|
+
catch (err) {
|
|
1356
|
+
return `[SYSTEM: update_session_summary failed: ${err?.message || String(err)}]`;
|
|
1243
1357
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1358
|
+
},
|
|
1359
|
+
sendSessionMessage: async (args) => {
|
|
1360
|
+
try {
|
|
1361
|
+
if (isReadOnlyTuner())
|
|
1362
|
+
return `[SYSTEM: send_session_message is disabled for read-only agent-tuner sessions.]`;
|
|
1363
|
+
const targetSessionId = String(args.session_id || "").replace(/^session-/, "");
|
|
1364
|
+
if (!targetSessionId)
|
|
1365
|
+
return `[SYSTEM: send_session_message failed: session_id is required.]`;
|
|
1366
|
+
if (targetSessionId === input.sessionId)
|
|
1367
|
+
return `[SYSTEM: send_session_message failed: target session is this session.]`;
|
|
1368
|
+
if (!args.subject?.trim() || !args.body?.trim())
|
|
1369
|
+
return `[SYSTEM: send_session_message failed: subject and body are required.]`;
|
|
1370
|
+
if (args.body.length > 8192)
|
|
1371
|
+
return `[SYSTEM: send_session_message failed: body exceeds 8 KB.]`;
|
|
1372
|
+
if (!catalog)
|
|
1373
|
+
return `[SYSTEM: send_session_message failed: CMS catalog is unavailable.]`;
|
|
1374
|
+
const sdkClient = await getInlineClient();
|
|
1375
|
+
const allowedReasons = new Set(["help", "guidance", "fact-request", "status-request", "handoff"]);
|
|
1376
|
+
const reason = allowedReasons.has(String(args.reason || "")) ? args.reason : undefined;
|
|
1377
|
+
const { requestId } = await sendInternalSessionMessage({
|
|
1378
|
+
catalog,
|
|
1379
|
+
duroxideClient: sdkClient._getDuroxideClient(),
|
|
1380
|
+
}, {
|
|
1381
|
+
fromSessionId: input.sessionId,
|
|
1382
|
+
toSessionId: targetSessionId,
|
|
1383
|
+
subject: args.subject,
|
|
1384
|
+
body: args.body,
|
|
1385
|
+
reason,
|
|
1386
|
+
expectsResponse: args.expects_response === true,
|
|
1387
|
+
expiresAt: args.expires_at,
|
|
1388
|
+
});
|
|
1389
|
+
return `[SYSTEM: Cross-session message queued. Request ID: ${requestId}. Target: session-${targetSessionId}.]`;
|
|
1247
1390
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return `[SYSTEM: reply_session_message is disabled for read-only agent-tuner sessions.]`;
|
|
1294
|
-
const targetSessionId = String(args.session_id || "").replace(/^session-/, "");
|
|
1295
|
-
if (!targetSessionId || !args.request_id || !args.body?.trim())
|
|
1296
|
-
return `[SYSTEM: reply_session_message failed: request_id, session_id, and body are required.]`;
|
|
1297
|
-
if (!catalog)
|
|
1298
|
-
return `[SYSTEM: reply_session_message failed: CMS catalog is unavailable.]`;
|
|
1299
|
-
const sdkClient = await getInlineClient();
|
|
1300
|
-
const allowedVerdicts = new Set(["answered", "declined", "blocked", "stale"]);
|
|
1301
|
-
const verdict = allowedVerdicts.has(String(args.verdict || "")) ? args.verdict : "answered";
|
|
1302
|
-
await replyInternalSessionMessage({
|
|
1303
|
-
catalog,
|
|
1304
|
-
duroxideClient: sdkClient._getDuroxideClient(),
|
|
1305
|
-
}, {
|
|
1306
|
-
requestId: args.request_id,
|
|
1307
|
-
fromSessionId: input.sessionId,
|
|
1308
|
-
toSessionId: targetSessionId,
|
|
1309
|
-
verdict,
|
|
1310
|
-
body: args.body,
|
|
1311
|
-
});
|
|
1312
|
-
return `[SYSTEM: Cross-session reply queued for request ${args.request_id}. Target: session-${targetSessionId}.]`;
|
|
1313
|
-
}
|
|
1314
|
-
catch (err) {
|
|
1315
|
-
return `[SYSTEM: reply_session_message failed: ${err?.message || String(err)}]`;
|
|
1316
|
-
}
|
|
1317
|
-
},
|
|
1318
|
-
completeAgent: async (args) => {
|
|
1319
|
-
try {
|
|
1320
|
-
const child = await resolveManagedChild(args.agent_id);
|
|
1321
|
-
let builtOutcome = null;
|
|
1322
|
-
if (catalog) {
|
|
1323
|
-
const existing = await catalog.getChildOutcome(child.sessionId).catch(() => null);
|
|
1324
|
-
const hasExistingResult = Boolean(normalizeJsonObject(existing?.resultJson?.current));
|
|
1325
|
-
if (args.result || (existing?.contractJson && !hasExistingResult)) {
|
|
1326
|
-
builtOutcome = buildResultJson(args.result, child, existing?.contractJson ?? null, existing?.resultJson ?? null, "success", "completed_without_result");
|
|
1391
|
+
catch (err) {
|
|
1392
|
+
return `[SYSTEM: send_session_message failed: ${err?.message || String(err)}]`;
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
replySessionMessage: async (args) => {
|
|
1396
|
+
try {
|
|
1397
|
+
if (isReadOnlyTuner())
|
|
1398
|
+
return `[SYSTEM: reply_session_message is disabled for read-only agent-tuner sessions.]`;
|
|
1399
|
+
const targetSessionId = String(args.session_id || "").replace(/^session-/, "");
|
|
1400
|
+
if (!targetSessionId || !args.request_id || !args.body?.trim())
|
|
1401
|
+
return `[SYSTEM: reply_session_message failed: request_id, session_id, and body are required.]`;
|
|
1402
|
+
if (!catalog)
|
|
1403
|
+
return `[SYSTEM: reply_session_message failed: CMS catalog is unavailable.]`;
|
|
1404
|
+
const sdkClient = await getInlineClient();
|
|
1405
|
+
const allowedVerdicts = new Set(["answered", "declined", "blocked", "stale"]);
|
|
1406
|
+
const verdict = allowedVerdicts.has(String(args.verdict || "")) ? args.verdict : "answered";
|
|
1407
|
+
await replyInternalSessionMessage({
|
|
1408
|
+
catalog,
|
|
1409
|
+
duroxideClient: sdkClient._getDuroxideClient(),
|
|
1410
|
+
}, {
|
|
1411
|
+
requestId: args.request_id,
|
|
1412
|
+
fromSessionId: input.sessionId,
|
|
1413
|
+
toSessionId: targetSessionId,
|
|
1414
|
+
verdict,
|
|
1415
|
+
body: args.body,
|
|
1416
|
+
});
|
|
1417
|
+
return `[SYSTEM: Cross-session reply queued for request ${args.request_id}. Target: session-${targetSessionId}.]`;
|
|
1418
|
+
}
|
|
1419
|
+
catch (err) {
|
|
1420
|
+
return `[SYSTEM: reply_session_message failed: ${err?.message || String(err)}]`;
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
completeAgent: async (args) => {
|
|
1424
|
+
try {
|
|
1425
|
+
const child = await resolveManagedChild(args.agent_id);
|
|
1426
|
+
let builtOutcome = null;
|
|
1427
|
+
if (catalog) {
|
|
1428
|
+
const existing = await catalog.getChildOutcome(child.sessionId).catch(() => null);
|
|
1429
|
+
const hasExistingResult = Boolean(normalizeJsonObject(existing?.resultJson?.current));
|
|
1430
|
+
if (args.result || (existing?.contractJson && !hasExistingResult)) {
|
|
1431
|
+
builtOutcome = buildResultJson(args.result, child, existing?.contractJson ?? null, existing?.resultJson ?? null, "success", "completed_without_result");
|
|
1432
|
+
}
|
|
1433
|
+
if (builtOutcome?.strictBlocked) {
|
|
1434
|
+
return `[SYSTEM: complete_agent blocked by strict contract validation for ${child.orchId}: ${builtOutcome.violations.map(v => v.message || v.code).join("; ")}]`;
|
|
1435
|
+
}
|
|
1327
1436
|
}
|
|
1328
|
-
if (builtOutcome
|
|
1329
|
-
|
|
1437
|
+
if (catalog && builtOutcome) {
|
|
1438
|
+
await cmsRetryCritical(`runTurn.complete.upsertChildOutcome result child=${child.sessionId}`, () => catalog.upsertChildOutcome({
|
|
1439
|
+
childSessionId: child.sessionId,
|
|
1440
|
+
parentSessionId: input.sessionId,
|
|
1441
|
+
resultJson: builtOutcome.resultJson,
|
|
1442
|
+
verdict: builtOutcome.verdict,
|
|
1443
|
+
summary: builtOutcome.summary,
|
|
1444
|
+
completedAt: new Date(),
|
|
1445
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1330
1446
|
}
|
|
1447
|
+
const sdkClient = await getInlineClient();
|
|
1448
|
+
const cmdId = `done-inline-${Date.now()}`;
|
|
1449
|
+
await sdkClient._getDuroxideClient().enqueueEvent(child.orchId, "messages", JSON.stringify({ type: "cmd", cmd: "done", id: cmdId, args: { reason: "Completed by parent" } }));
|
|
1450
|
+
return `[SYSTEM: Graceful completion requested for sub-agent ${child.orchId}. ` +
|
|
1451
|
+
`Use check_agents or wait_for_agents to observe final completion.]`;
|
|
1331
1452
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
childSessionId: child.sessionId,
|
|
1335
|
-
parentSessionId: input.sessionId,
|
|
1336
|
-
resultJson: builtOutcome.resultJson,
|
|
1337
|
-
verdict: builtOutcome.verdict,
|
|
1338
|
-
summary: builtOutcome.summary,
|
|
1339
|
-
completedAt: new Date(),
|
|
1340
|
-
}), (msg) => activityCtx.traceInfo(msg));
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
return `[SYSTEM: complete_agent failed: ${err?.message || String(err)}]`;
|
|
1341
1455
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1456
|
+
},
|
|
1457
|
+
cancelAgent: async (args) => {
|
|
1458
|
+
try {
|
|
1459
|
+
const child = await resolveManagedChild(args.agent_id);
|
|
1460
|
+
let builtOutcome = null;
|
|
1461
|
+
if (catalog) {
|
|
1462
|
+
const existing = await catalog.getChildOutcome(child.sessionId).catch(() => null);
|
|
1463
|
+
const hasExistingResult = Boolean(normalizeJsonObject(existing?.resultJson?.current));
|
|
1464
|
+
if (args.partial_result || (existing?.contractJson && !hasExistingResult)) {
|
|
1465
|
+
builtOutcome = buildResultJson(args.partial_result, child, existing?.contractJson ?? null, existing?.resultJson ?? null, "cancelled", "completed_without_result");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (catalog && builtOutcome) {
|
|
1469
|
+
await cmsRetryCritical(`runTurn.cancel.upsertChildOutcome partial child=${child.sessionId}`, () => catalog.upsertChildOutcome({
|
|
1470
|
+
childSessionId: child.sessionId,
|
|
1471
|
+
parentSessionId: input.sessionId,
|
|
1472
|
+
resultJson: builtOutcome.resultJson,
|
|
1473
|
+
verdict: builtOutcome.verdict,
|
|
1474
|
+
summary: builtOutcome.summary,
|
|
1475
|
+
completedAt: new Date(),
|
|
1476
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1361
1477
|
}
|
|
1478
|
+
const sdkClient = await getInlineClient();
|
|
1479
|
+
const cmdId = `cancel-inline-${Date.now()}`;
|
|
1480
|
+
await sdkClient._getDuroxideClient().enqueueEvent(child.orchId, "messages", JSON.stringify({ type: "cmd", cmd: "cancel", id: cmdId, args: { reason: args.reason ?? "Cancelled by parent" } }));
|
|
1481
|
+
return `[SYSTEM: Graceful cancellation requested for sub-agent ${child.orchId}. ` +
|
|
1482
|
+
`Use check_agents or wait_for_agents to observe final termination.${args.reason ? ` Reason: ${args.reason}` : ""}]`;
|
|
1362
1483
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
childSessionId: child.sessionId,
|
|
1366
|
-
parentSessionId: input.sessionId,
|
|
1367
|
-
resultJson: builtOutcome.resultJson,
|
|
1368
|
-
verdict: builtOutcome.verdict,
|
|
1369
|
-
summary: builtOutcome.summary,
|
|
1370
|
-
completedAt: new Date(),
|
|
1371
|
-
}), (msg) => activityCtx.traceInfo(msg));
|
|
1484
|
+
catch (err) {
|
|
1485
|
+
return `[SYSTEM: cancel_agent failed: ${err?.message || String(err)}]`;
|
|
1372
1486
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const sdkClient = await getInlineClient();
|
|
1387
|
-
if (child.status === "completed" || child.status === "failed" || child.status === "cancelled") {
|
|
1388
|
-
await sdkClient.deleteSession(child.sessionId);
|
|
1389
|
-
return `[SYSTEM: Sub-agent ${child.orchId} has been deleted.${args.reason ? ` Reason: ${args.reason}` : ""}]`;
|
|
1487
|
+
},
|
|
1488
|
+
deleteAgent: async (args) => {
|
|
1489
|
+
try {
|
|
1490
|
+
const child = await resolveManagedChild(args.agent_id);
|
|
1491
|
+
const sdkClient = await getInlineClient();
|
|
1492
|
+
if (child.status === "completed" || child.status === "failed" || child.status === "cancelled") {
|
|
1493
|
+
await sdkClient.deleteSession(child.sessionId);
|
|
1494
|
+
return `[SYSTEM: Sub-agent ${child.orchId} has been deleted.${args.reason ? ` Reason: ${args.reason}` : ""}]`;
|
|
1495
|
+
}
|
|
1496
|
+
const cmdId = `delete-inline-${Date.now()}`;
|
|
1497
|
+
await sdkClient._getDuroxideClient().enqueueEvent(child.orchId, "messages", JSON.stringify({ type: "cmd", cmd: "delete", id: cmdId, args: { reason: args.reason ?? "Deleted by parent" } }));
|
|
1498
|
+
return `[SYSTEM: Graceful deletion requested for sub-agent ${child.orchId}. ` +
|
|
1499
|
+
`It will cancel its descendants first and then delete itself.${args.reason ? ` Reason: ${args.reason}` : ""}]`;
|
|
1390
1500
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1501
|
+
catch (err) {
|
|
1502
|
+
return `[SYSTEM: delete_agent failed: ${err?.message || String(err)}]`;
|
|
1503
|
+
}
|
|
1504
|
+
},
|
|
1505
|
+
};
|
|
1506
|
+
// Cooperative cancellation: poll for lock steal
|
|
1507
|
+
let cancelled = false;
|
|
1508
|
+
cancelPoll = setInterval(() => {
|
|
1509
|
+
if (activityCtx.isCancelled()) {
|
|
1510
|
+
cancelled = true;
|
|
1511
|
+
session?.abort?.();
|
|
1512
|
+
if (cancelPoll)
|
|
1513
|
+
clearInterval(cancelPoll);
|
|
1398
1514
|
}
|
|
1399
|
-
},
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
}
|
|
1410
|
-
}, 2_000);
|
|
1411
|
-
// Build onEvent callback: write each non-ephemeral event to CMS as it fires.
|
|
1412
|
-
// We track every in-flight CMS recordEvents promise so we can flush them
|
|
1413
|
-
// before posting `session.turn_completed`. Without this barrier the
|
|
1414
|
-
// turn_completed insert can race ahead of the SDK's `assistant.message`
|
|
1415
|
-
// insert and CMS will assign the smaller `seq` to turn_completed,
|
|
1416
|
-
// breaking event-ordering invariants downstream (see cms-seq-nodemap).
|
|
1417
|
-
const pendingEventWrites = [];
|
|
1418
|
-
const trackEventWrite = (promise) => {
|
|
1419
|
-
if (!promise || typeof promise.then !== "function")
|
|
1420
|
-
return;
|
|
1421
|
-
pendingEventWrites.push(promise.catch(() => { }));
|
|
1422
|
-
};
|
|
1423
|
-
const EPHEMERAL_TYPES = new Set([
|
|
1424
|
-
"assistant.message_delta",
|
|
1425
|
-
"assistant.streaming_delta",
|
|
1426
|
-
"assistant.reasoning_delta",
|
|
1427
|
-
"user.message", // Already recorded explicitly above — skip the SDK's duplicate
|
|
1428
|
-
]);
|
|
1429
|
-
const onEvent = catalog
|
|
1430
|
-
? (event) => {
|
|
1431
|
-
if (EPHEMERAL_TYPES.has(event.eventType))
|
|
1515
|
+
}, 2_000);
|
|
1516
|
+
// Build onEvent callback: write each non-ephemeral event to CMS as it fires.
|
|
1517
|
+
// We track every in-flight CMS recordEvents promise so we can flush them
|
|
1518
|
+
// before posting `session.turn_completed`. Without this barrier the
|
|
1519
|
+
// turn_completed insert can race ahead of the SDK's `assistant.message`
|
|
1520
|
+
// insert and CMS will assign the smaller `seq` to turn_completed,
|
|
1521
|
+
// breaking event-ordering invariants downstream (see cms-seq-nodemap).
|
|
1522
|
+
const pendingEventWrites = [];
|
|
1523
|
+
const trackEventWrite = (promise) => {
|
|
1524
|
+
if (!promise || typeof promise.then !== "function")
|
|
1432
1525
|
return;
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
turnTelemetry.tokensInput += usageUpsert.tokensInputIncrement ?? 0;
|
|
1456
|
-
turnTelemetry.tokensOutput += usageUpsert.tokensOutputIncrement ?? 0;
|
|
1457
|
-
turnTelemetry.tokensCacheRead += usageUpsert.tokensCacheReadIncrement ?? 0;
|
|
1458
|
-
turnTelemetry.tokensCacheWrite += usageUpsert.tokensCacheWriteIncrement ?? 0;
|
|
1526
|
+
pendingEventWrites.push(promise.catch(() => { }));
|
|
1527
|
+
};
|
|
1528
|
+
const EPHEMERAL_TYPES = new Set([
|
|
1529
|
+
"assistant.message_delta",
|
|
1530
|
+
"assistant.streaming_delta",
|
|
1531
|
+
"assistant.reasoning_delta",
|
|
1532
|
+
"user.message", // Already recorded explicitly above — skip the SDK's duplicate
|
|
1533
|
+
]);
|
|
1534
|
+
const onEvent = catalog
|
|
1535
|
+
? (event) => {
|
|
1536
|
+
if (EPHEMERAL_TYPES.has(event.eventType))
|
|
1537
|
+
return;
|
|
1538
|
+
const persistedEvent = summarizeSdkSystemPromptEchoEvent(event);
|
|
1539
|
+
if (!persistedEvent)
|
|
1540
|
+
return;
|
|
1541
|
+
if (event.eventType === "session.wait_started") {
|
|
1542
|
+
const data = (event.data ?? {});
|
|
1543
|
+
void cmsRetryBestEffort(`runTurn.onEvent updateSession state=waiting session=${input.sessionId}`, () => catalog.updateSession(input.sessionId, {
|
|
1544
|
+
state: "waiting",
|
|
1545
|
+
waitReason: data.reason ?? null,
|
|
1546
|
+
lastActiveAt: new Date(),
|
|
1547
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1459
1548
|
}
|
|
1549
|
+
else if (event.eventType === "session.input_required_started") {
|
|
1550
|
+
const data = (event.data ?? {});
|
|
1551
|
+
void cmsRetryBestEffort(`runTurn.onEvent updateSession state=input_required session=${input.sessionId}`, () => catalog.updateSession(input.sessionId, {
|
|
1552
|
+
state: "input_required",
|
|
1553
|
+
waitReason: data.question ?? null,
|
|
1554
|
+
lastActiveAt: new Date(),
|
|
1555
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1556
|
+
}
|
|
1557
|
+
if (event.eventType === "assistant.usage") {
|
|
1558
|
+
const usageUpsert = buildUsageSummaryUpsert(event.data);
|
|
1559
|
+
if (usageUpsert) {
|
|
1560
|
+
turnTelemetry.tokensInput += usageUpsert.tokensInputIncrement ?? 0;
|
|
1561
|
+
turnTelemetry.tokensOutput += usageUpsert.tokensOutputIncrement ?? 0;
|
|
1562
|
+
turnTelemetry.tokensCacheRead += usageUpsert.tokensCacheReadIncrement ?? 0;
|
|
1563
|
+
turnTelemetry.tokensCacheWrite += usageUpsert.tokensCacheWriteIncrement ?? 0;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
else if (event.eventType === "tool.execution_start") {
|
|
1567
|
+
turnTelemetry.toolCalls += 1;
|
|
1568
|
+
const eventData = normalizeEventData(event.data);
|
|
1569
|
+
const toolName = typeof eventData?.toolName === "string"
|
|
1570
|
+
? eventData.toolName
|
|
1571
|
+
: typeof eventData?.name === "string"
|
|
1572
|
+
? eventData.name
|
|
1573
|
+
: undefined;
|
|
1574
|
+
if (toolName)
|
|
1575
|
+
turnTelemetry.toolNames.add(toolName);
|
|
1576
|
+
}
|
|
1577
|
+
else if (event.eventType === "tool.execution_complete" && isFailureToolCompletion(event.data)) {
|
|
1578
|
+
turnTelemetry.toolErrors += 1;
|
|
1579
|
+
}
|
|
1580
|
+
// Best-effort with one transient retry. trackEventWrite tracks
|
|
1581
|
+
// the wrapped promise so the post-turn barrier waits for the
|
|
1582
|
+
// retry to settle before emitting turn_completed.
|
|
1583
|
+
const writePromise = cmsRetryBestEffort(`runTurn.onEvent recordEvent ${persistedEvent.eventType} session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [persistedEvent], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1584
|
+
trackEventWrite(writePromise);
|
|
1460
1585
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1586
|
+
: undefined;
|
|
1587
|
+
// Record the user prompt as a CMS event before running the turn.
|
|
1588
|
+
// Skip internal timer continuation prompts — they're system-generated, not user input.
|
|
1589
|
+
const isTimerPrompt = /^The \d+ second wait is now complete\./i.test(input.prompt);
|
|
1590
|
+
const isRetryAttempt = (input.retryCount ?? 0) > 0;
|
|
1591
|
+
if (catalog && input.config.turnSystemPrompt && !isRetryAttempt) {
|
|
1592
|
+
const persistedSystemPrompt = decorateRehydrationSystemPrompt(input.config.turnSystemPrompt, workerNodeId);
|
|
1593
|
+
void cmsRetryBestEffort(`runTurn.recordEvent system-prompt session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1594
|
+
eventType: "system.message",
|
|
1595
|
+
data: { content: persistedSystemPrompt },
|
|
1596
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1597
|
+
}
|
|
1598
|
+
if (catalog && !isTimerPrompt && !input.bootstrap && !isRetryAttempt) {
|
|
1599
|
+
const promptEventType = isInternalSystemPrompt(input.prompt) ? "system.message" : "user.message";
|
|
1600
|
+
// v1.0.47: when the orchestration tagged the turn with one or
|
|
1601
|
+
// more clientMessageIds (the UI-generated identities of the
|
|
1602
|
+
// contributing local outbox items), persist them on the
|
|
1603
|
+
// durable user.message event so the client can ack/cancel by
|
|
1604
|
+
// exact id rather than text match.
|
|
1605
|
+
const incomingClientMessageIds = Array.isArray(input.clientMessageIds)
|
|
1606
|
+
? input.clientMessageIds.filter((id) => typeof id === "string" && id)
|
|
1607
|
+
: [];
|
|
1608
|
+
const eventData = { content: input.prompt };
|
|
1609
|
+
if (incomingClientMessageIds.length > 0) {
|
|
1610
|
+
eventData.clientMessageIds = incomingClientMessageIds;
|
|
1474
1611
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1612
|
+
const capturedPromptEventType = promptEventType;
|
|
1613
|
+
void cmsRetryBestEffort(`runTurn.recordEvent ${capturedPromptEventType} session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1614
|
+
eventType: capturedPromptEventType,
|
|
1615
|
+
data: eventData,
|
|
1616
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1480
1617
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
data: { content: persistedSystemPrompt },
|
|
1491
|
-
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1492
|
-
}
|
|
1493
|
-
if (catalog && !isTimerPrompt && !input.bootstrap && !isRetryAttempt) {
|
|
1494
|
-
const promptEventType = isInternalSystemPrompt(input.prompt) ? "system.message" : "user.message";
|
|
1495
|
-
// v1.0.47: when the orchestration tagged the turn with one or
|
|
1496
|
-
// more clientMessageIds (the UI-generated identities of the
|
|
1497
|
-
// contributing local outbox items), persist them on the
|
|
1498
|
-
// durable user.message event so the client can ack/cancel by
|
|
1499
|
-
// exact id rather than text match.
|
|
1500
|
-
const incomingClientMessageIds = Array.isArray(input.clientMessageIds)
|
|
1501
|
-
? input.clientMessageIds.filter((id) => typeof id === "string" && id)
|
|
1502
|
-
: [];
|
|
1503
|
-
const eventData = { content: input.prompt };
|
|
1504
|
-
if (incomingClientMessageIds.length > 0) {
|
|
1505
|
-
eventData.clientMessageIds = incomingClientMessageIds;
|
|
1618
|
+
// Mark session as "running" in CMS before the turn, and publish the
|
|
1619
|
+
// in-flight turn index so stopSessionTurn() can address the
|
|
1620
|
+
// turn-scoped stop queue (stopTurn.<turnIndex>).
|
|
1621
|
+
if (catalog) {
|
|
1622
|
+
await cmsRetryBestEffort(`runTurn.preTurn updateSession state=running session=${input.sessionId}`, () => catalog.updateSession(input.sessionId, {
|
|
1623
|
+
state: "running",
|
|
1624
|
+
lastActiveAt: new Date(),
|
|
1625
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1626
|
+
await cmsRetryBestEffort(`runTurn.preTurn setActiveTurnIndex session=${input.sessionId} turn=${input.turnIndex ?? 0}`, () => catalog.setActiveTurnIndex(input.sessionId, input.turnIndex ?? 0), (msg) => activityCtx.traceInfo(msg));
|
|
1506
1627
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
eventType: capturedPromptEventType,
|
|
1510
|
-
data: eventData,
|
|
1511
|
-
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1512
|
-
}
|
|
1513
|
-
// Mark session as "running" in CMS before the turn, and publish the
|
|
1514
|
-
// in-flight turn index so stopSessionTurn() can address the
|
|
1515
|
-
// turn-scoped stop queue (stopTurn.<turnIndex>).
|
|
1516
|
-
if (catalog) {
|
|
1517
|
-
await cmsRetryBestEffort(`runTurn.preTurn updateSession state=running session=${input.sessionId}`, () => catalog.updateSession(input.sessionId, {
|
|
1518
|
-
state: "running",
|
|
1519
|
-
lastActiveAt: new Date(),
|
|
1520
|
-
}), (msg) => activityCtx.traceInfo(msg));
|
|
1521
|
-
await cmsRetryBestEffort(`runTurn.preTurn setActiveTurnIndex session=${input.sessionId} turn=${input.turnIndex ?? 0}`, () => catalog.setActiveTurnIndex(input.sessionId, input.turnIndex ?? 0), (msg) => activityCtx.traceInfo(msg));
|
|
1522
|
-
}
|
|
1523
|
-
activityCtx.traceInfo(`[runTurn] invoking ManagedSession.runTurn for ${input.sessionId}`);
|
|
1524
|
-
// Record turn_started CMS event
|
|
1525
|
-
if (catalog) {
|
|
1526
|
-
void cmsRetryBestEffort(`runTurn.recordEvent turn_started session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1527
|
-
eventType: "session.turn_started",
|
|
1528
|
-
data: { iteration: input.turnIndex ?? 0 },
|
|
1529
|
-
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1530
|
-
}
|
|
1531
|
-
const runTurnWithPrompt = async (targetSession, prompt) => {
|
|
1532
|
-
return await targetSession.runTurn(prompt, {
|
|
1533
|
-
onEvent,
|
|
1534
|
-
modelSummary: sessionManager.getModelSummary(),
|
|
1535
|
-
bootstrap: input.bootstrap,
|
|
1536
|
-
requiredTool: input.requiredTool,
|
|
1537
|
-
cycleOrigin: input.cycleOrigin,
|
|
1538
|
-
turnIndex: input.turnIndex,
|
|
1539
|
-
controlToolBridge,
|
|
1540
|
-
});
|
|
1541
|
-
};
|
|
1542
|
-
let result = await runTurnWithPrompt(session, effectivePrompt);
|
|
1543
|
-
if (result.type === "error" && isLiveSessionLostErrorMessage(result.message)) {
|
|
1544
|
-
activityCtx.traceInfo(`[runTurn] live Copilot session lost for ${input.sessionId}; invalidating warm session and attempting recovery`);
|
|
1545
|
-
await sessionManager.invalidateWarmSession(input.sessionId, { lockHeld: true }).catch((err) => {
|
|
1546
|
-
activityCtx.traceInfo(`[runTurn] warm-session invalidation failed (non-fatal): ${err?.message ?? err}`);
|
|
1547
|
-
});
|
|
1628
|
+
activityCtx.traceInfo(`[runTurn] invoking ManagedSession.runTurn for ${input.sessionId}`);
|
|
1629
|
+
// Record turn_started CMS event
|
|
1548
1630
|
if (catalog) {
|
|
1549
|
-
|
|
1550
|
-
eventType: "
|
|
1551
|
-
data: {
|
|
1552
|
-
content: "The runtime recovered this session after the worker lost the live Copilot session. " +
|
|
1553
|
-
"Some very recent in-memory state may have been lost.",
|
|
1554
|
-
},
|
|
1631
|
+
void cmsRetryBestEffort(`runTurn.recordEvent turn_started session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1632
|
+
eventType: "session.turn_started",
|
|
1633
|
+
data: { iteration: input.turnIndex ?? 0 },
|
|
1555
1634
|
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1556
1635
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1636
|
+
const runTurnWithPrompt = async (targetSession, prompt) => {
|
|
1637
|
+
return await targetSession.runTurn(prompt, {
|
|
1638
|
+
onEvent,
|
|
1639
|
+
modelSummary: sessionManager.getModelSummary(),
|
|
1640
|
+
bootstrap: input.bootstrap,
|
|
1641
|
+
requiredTool: input.requiredTool,
|
|
1642
|
+
cycleOrigin: input.cycleOrigin,
|
|
1559
1643
|
turnIndex: input.turnIndex,
|
|
1560
|
-
|
|
1561
|
-
lockHeld: true,
|
|
1644
|
+
controlToolBridge,
|
|
1562
1645
|
});
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
const recoveryMessage = err?.message || String(err);
|
|
1566
|
-
const fatalMessage = isMissingSessionStateErrorMessage(recoveryMessage)
|
|
1567
|
-
? buildUnrecoverableSessionLossMessage(input.sessionId, stripMissingSessionStatePrefix(recoveryMessage))
|
|
1568
|
-
: buildUnrecoverableSessionLossMessage(input.sessionId, recoveryMessage);
|
|
1569
|
-
activityCtx.traceInfo(`[runTurn] unrecoverable session loss for ${input.sessionId}: ${fatalMessage}`);
|
|
1570
|
-
return await failForMissingState(fatalMessage);
|
|
1571
|
-
}
|
|
1572
|
-
const recoveredPrompt = mergePromptSections([SESSION_RECOVERY_NOTICE, input.prompt]) || input.prompt;
|
|
1573
|
-
result = await runTurnWithPrompt(session, recoveredPrompt);
|
|
1646
|
+
};
|
|
1647
|
+
let result = await runTurnWithPrompt(session, effectivePrompt);
|
|
1574
1648
|
if (result.type === "error" && isLiveSessionLostErrorMessage(result.message)) {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
if (result.type === "error" && isToolCallTranscriptCorruptionErrorMessage(result.message)) {
|
|
1581
|
-
const transcriptError = result.message;
|
|
1582
|
-
activityCtx.traceInfo(`[runTurn] corrupted live Copilot transcript for ${input.sessionId}; resetting stored session state and attempting fresh-session replay`);
|
|
1583
|
-
await sessionManager.resetSessionState(input.sessionId, { lockHeld: true }).catch((err) => {
|
|
1584
|
-
activityCtx.traceInfo(`[runTurn] stored-session reset failed (non-fatal): ${err?.message ?? err}`);
|
|
1585
|
-
});
|
|
1586
|
-
await recordLossyHandoffEvent(catalog, input.sessionId, workerNodeId, {
|
|
1587
|
-
cause: "corrupted_tool_call_transcript_during_run_turn",
|
|
1588
|
-
message: "The runtime detected an inconsistent live Copilot transcript and recreated a fresh session to replay the pending turn.",
|
|
1589
|
-
detail: transcriptError,
|
|
1590
|
-
error: transcriptError,
|
|
1591
|
-
recoveryMode: "fresh_session_replay",
|
|
1592
|
-
nextStep: "replay_pending_turn_with_recreated_copilot_session",
|
|
1593
|
-
...(input.turnIndex != null ? { iteration: input.turnIndex } : {}),
|
|
1594
|
-
}, (failureMessage) => activityCtx.traceInfo(`[runTurn] ${failureMessage}`));
|
|
1595
|
-
if (catalog) {
|
|
1596
|
-
await cmsRetryBestEffort(`runTurn.recordEvent system.message-corrupted-transcript session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1597
|
-
eventType: "system.message",
|
|
1598
|
-
data: {
|
|
1599
|
-
content: "The runtime recreated this session after the live Copilot transcript became inconsistent. " +
|
|
1600
|
-
"Some recent in-memory work may be missing or partially executed.",
|
|
1601
|
-
},
|
|
1602
|
-
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1603
|
-
}
|
|
1604
|
-
try {
|
|
1605
|
-
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
1606
|
-
turnIndex: 0,
|
|
1607
|
-
trace,
|
|
1608
|
-
lockHeld: true,
|
|
1649
|
+
activityCtx.traceInfo(`[runTurn] live Copilot session lost for ${input.sessionId}; invalidating warm session and attempting recovery`);
|
|
1650
|
+
await sessionManager.invalidateWarmSession(input.sessionId, { lockHeld: true }).catch((err) => {
|
|
1651
|
+
activityCtx.traceInfo(`[runTurn] warm-session invalidation failed (non-fatal): ${err?.message ?? err}`);
|
|
1609
1652
|
});
|
|
1653
|
+
if (catalog) {
|
|
1654
|
+
await cmsRetryBestEffort(`runTurn.recordEvent system.message-recovery session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1655
|
+
eventType: "system.message",
|
|
1656
|
+
data: {
|
|
1657
|
+
content: "The runtime recovered this session after the worker lost the live Copilot session. " +
|
|
1658
|
+
"Some very recent in-memory state may have been lost.",
|
|
1659
|
+
},
|
|
1660
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
1664
|
+
turnIndex: input.turnIndex,
|
|
1665
|
+
trace,
|
|
1666
|
+
lockHeld: true,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
catch (err) {
|
|
1670
|
+
const recoveryMessage = err?.message || String(err);
|
|
1671
|
+
const fatalMessage = isMissingSessionStateErrorMessage(recoveryMessage)
|
|
1672
|
+
? buildUnrecoverableSessionLossMessage(input.sessionId, stripMissingSessionStatePrefix(recoveryMessage))
|
|
1673
|
+
: buildUnrecoverableSessionLossMessage(input.sessionId, recoveryMessage);
|
|
1674
|
+
activityCtx.traceInfo(`[runTurn] unrecoverable session loss for ${input.sessionId}: ${fatalMessage}`);
|
|
1675
|
+
return await failForMissingState(fatalMessage);
|
|
1676
|
+
}
|
|
1677
|
+
const recoveredPrompt = mergePromptSections([SESSION_RECOVERY_NOTICE, input.prompt]) || input.prompt;
|
|
1678
|
+
result = await runTurnWithPrompt(session, recoveredPrompt);
|
|
1679
|
+
if (result.type === "error" && isLiveSessionLostErrorMessage(result.message)) {
|
|
1680
|
+
const fatalMessage = buildUnrecoverableSessionLossMessage(input.sessionId, result.message);
|
|
1681
|
+
activityCtx.traceInfo(`[runTurn] recovery re-run still reported lost session for ${input.sessionId}: ${fatalMessage}`);
|
|
1682
|
+
return await failForMissingState(fatalMessage);
|
|
1683
|
+
}
|
|
1610
1684
|
}
|
|
1611
|
-
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1685
|
+
if (result.type === "error" && isToolCallTranscriptCorruptionErrorMessage(result.message)) {
|
|
1686
|
+
const transcriptError = result.message;
|
|
1687
|
+
activityCtx.traceInfo(`[runTurn] corrupted live Copilot transcript for ${input.sessionId}; resetting stored session state and attempting fresh-session replay`);
|
|
1688
|
+
await sessionManager.resetSessionState(input.sessionId, { lockHeld: true }).catch((err) => {
|
|
1689
|
+
activityCtx.traceInfo(`[runTurn] stored-session reset failed (non-fatal): ${err?.message ?? err}`);
|
|
1690
|
+
});
|
|
1691
|
+
await recordLossyHandoffEvent(catalog, input.sessionId, workerNodeId, {
|
|
1692
|
+
cause: "corrupted_tool_call_transcript_during_run_turn",
|
|
1693
|
+
message: "The runtime detected an inconsistent live Copilot transcript and recreated a fresh session to replay the pending turn.",
|
|
1694
|
+
detail: transcriptError,
|
|
1695
|
+
error: transcriptError,
|
|
1696
|
+
recoveryMode: "fresh_session_replay",
|
|
1697
|
+
nextStep: "replay_pending_turn_with_recreated_copilot_session",
|
|
1698
|
+
...(input.turnIndex != null ? { iteration: input.turnIndex } : {}),
|
|
1699
|
+
}, (failureMessage) => activityCtx.traceInfo(`[runTurn] ${failureMessage}`));
|
|
1700
|
+
if (catalog) {
|
|
1701
|
+
await cmsRetryBestEffort(`runTurn.recordEvent system.message-corrupted-transcript session=${input.sessionId}`, () => catalog.recordEvents(input.sessionId, [{
|
|
1702
|
+
eventType: "system.message",
|
|
1703
|
+
data: {
|
|
1704
|
+
content: "The runtime recreated this session after the live Copilot transcript became inconsistent. " +
|
|
1705
|
+
"Some recent in-memory work may be missing or partially executed.",
|
|
1706
|
+
},
|
|
1707
|
+
}], workerNodeId), (msg) => activityCtx.traceInfo(msg));
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
session = await sessionManager.getOrCreate(input.sessionId, runConfig, {
|
|
1711
|
+
turnIndex: 0,
|
|
1712
|
+
trace,
|
|
1713
|
+
lockHeld: true,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
catch (err) {
|
|
1717
|
+
const recoveryMessage = err?.message || String(err);
|
|
1718
|
+
const fatalMessage = `Live Copilot transcript became inconsistent for ${input.sessionId}, and fresh-session recovery failed. ${recoveryMessage}`;
|
|
1719
|
+
activityCtx.traceInfo(`[runTurn] unrecoverable corrupted transcript for ${input.sessionId}: ${fatalMessage}`);
|
|
1720
|
+
return { type: "error", message: fatalMessage };
|
|
1721
|
+
}
|
|
1722
|
+
const recoveredPrompt = mergePromptSections([CORRUPTED_TRANSCRIPT_REPLAY_NOTICE, input.prompt]) || input.prompt;
|
|
1723
|
+
result = await runTurnWithPrompt(session, recoveredPrompt);
|
|
1616
1724
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
reason: "waiting for parent answer",
|
|
1629
|
-
content: result.content.trim(),
|
|
1630
|
-
};
|
|
1631
|
-
}
|
|
1632
|
-
activityCtx.traceInfo(`[runTurn] ManagedSession.runTurn completed for ${input.sessionId} type=${result.type}`);
|
|
1633
|
-
// Drain event writes before the atomic post-turn writeback records
|
|
1634
|
-
// session.turn_completed.
|
|
1635
|
-
//
|
|
1636
|
-
// Ordering matters here: every per-event CMS write fired by the
|
|
1637
|
-
// SDK's onEvent callback (assistant.message, tool calls, usage,
|
|
1638
|
-
// etc.) must land in `session_events` with a smaller `seq` than
|
|
1639
|
-
// `session.turn_completed`, otherwise downstream consumers that
|
|
1640
|
-
// walk events in seq order (e.g. cms-seq-nodemap) see the turn
|
|
1641
|
-
// close before its own assistant.message.
|
|
1642
|
-
//
|
|
1643
|
-
// `runTurn` resolves once the SDK has emitted its final events,
|
|
1644
|
-
// but the corresponding `recordEvents` calls were dispatched
|
|
1645
|
-
// fire-and-forget. We therefore:
|
|
1646
|
-
// 1. Sleep 100ms to let any straggler onEvent callbacks fire
|
|
1647
|
-
// and enqueue their CMS write (quiesce window).
|
|
1648
|
-
// 2. Drain `pendingEventWrites` and await all of them with
|
|
1649
|
-
// allSettled — failed writes were already logged inline,
|
|
1650
|
-
// we only need the ordering guarantee.
|
|
1651
|
-
// 3. Await the turn_completed insert so subsequent activity
|
|
1652
|
-
// logic and any immediate CMS readers see a consistent tail.
|
|
1653
|
-
//
|
|
1654
|
-
// The 100ms is a deliberate, bounded pause. If it ever needs to
|
|
1655
|
-
// grow, prefer a real "SDK turn fully flushed" signal over a
|
|
1656
|
-
// larger sleep.
|
|
1657
|
-
if (catalog) {
|
|
1658
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1659
|
-
const pendingWritesAtBarrier = pendingEventWrites.splice(0);
|
|
1660
|
-
if (pendingWritesAtBarrier.length > 0) {
|
|
1661
|
-
await Promise.allSettled(pendingWritesAtBarrier);
|
|
1725
|
+
if (input.parentSessionId
|
|
1726
|
+
&& result.type === "completed"
|
|
1727
|
+
&& typeof result.content === "string"
|
|
1728
|
+
&& /^QUESTION FOR PARENT:/i.test(result.content.trim())) {
|
|
1729
|
+
result = {
|
|
1730
|
+
...result,
|
|
1731
|
+
type: "wait",
|
|
1732
|
+
seconds: 60,
|
|
1733
|
+
reason: "waiting for parent answer",
|
|
1734
|
+
content: result.content.trim(),
|
|
1735
|
+
};
|
|
1662
1736
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
updates.waitReason = null;
|
|
1737
|
+
activityCtx.traceInfo(`[runTurn] ManagedSession.runTurn completed for ${input.sessionId} type=${result.type}`);
|
|
1738
|
+
// Drain event writes before the atomic post-turn writeback records
|
|
1739
|
+
// session.turn_completed.
|
|
1740
|
+
//
|
|
1741
|
+
// Ordering matters here: every per-event CMS write fired by the
|
|
1742
|
+
// SDK's onEvent callback (assistant.message, tool calls, usage,
|
|
1743
|
+
// etc.) must land in `session_events` with a smaller `seq` than
|
|
1744
|
+
// `session.turn_completed`, otherwise downstream consumers that
|
|
1745
|
+
// walk events in seq order (e.g. cms-seq-nodemap) see the turn
|
|
1746
|
+
// close before its own assistant.message.
|
|
1747
|
+
//
|
|
1748
|
+
// `runTurn` resolves once the SDK has emitted its final events,
|
|
1749
|
+
// but the corresponding `recordEvents` calls were dispatched
|
|
1750
|
+
// fire-and-forget. We therefore:
|
|
1751
|
+
// 1. Sleep 100ms to let any straggler onEvent callbacks fire
|
|
1752
|
+
// and enqueue their CMS write (quiesce window).
|
|
1753
|
+
// 2. Drain `pendingEventWrites` and await all of them with
|
|
1754
|
+
// allSettled — failed writes were already logged inline,
|
|
1755
|
+
// we only need the ordering guarantee.
|
|
1756
|
+
// 3. Await the turn_completed insert so subsequent activity
|
|
1757
|
+
// logic and any immediate CMS readers see a consistent tail.
|
|
1758
|
+
//
|
|
1759
|
+
// The 100ms is a deliberate, bounded pause. If it ever needs to
|
|
1760
|
+
// grow, prefer a real "SDK turn fully flushed" signal over a
|
|
1761
|
+
// larger sleep.
|
|
1762
|
+
if (catalog) {
|
|
1763
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1764
|
+
const pendingWritesAtBarrier = pendingEventWrites.splice(0);
|
|
1765
|
+
if (pendingWritesAtBarrier.length > 0) {
|
|
1766
|
+
await Promise.allSettled(pendingWritesAtBarrier);
|
|
1767
|
+
}
|
|
1695
1768
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1769
|
+
if (cancelled)
|
|
1770
|
+
return { type: "cancelled" };
|
|
1771
|
+
// ── Activity-level writeback: sync turn result → CMS ──
|
|
1772
|
+
// This lets listSessions() read entirely from CMS without
|
|
1773
|
+
// hitting duroxide for every session's customStatus.
|
|
1774
|
+
if (catalog) {
|
|
1775
|
+
const statusMap = {
|
|
1776
|
+
completed: "idle", // orchestration decides idle vs completed; default to idle
|
|
1777
|
+
wait: "waiting",
|
|
1778
|
+
cron: "running",
|
|
1779
|
+
input_required: "input_required",
|
|
1780
|
+
error: "error",
|
|
1781
|
+
cancelled: "idle",
|
|
1782
|
+
stopped: "idle",
|
|
1783
|
+
spawn_agent: "running",
|
|
1784
|
+
message_agent: "running",
|
|
1785
|
+
check_agents: "running",
|
|
1786
|
+
wait_for_agents: "waiting",
|
|
1787
|
+
list_sessions: "running",
|
|
1788
|
+
complete_agent: "running",
|
|
1789
|
+
cancel_agent: "running",
|
|
1790
|
+
delete_agent: "running",
|
|
1791
|
+
};
|
|
1792
|
+
const liveStatus = statusMap[result.type] ?? "idle";
|
|
1793
|
+
const updates = {
|
|
1794
|
+
state: liveStatus,
|
|
1795
|
+
lastActiveAt: new Date(),
|
|
1796
|
+
};
|
|
1797
|
+
if (result.type === "error") {
|
|
1798
|
+
updates.lastError = result.message ?? null;
|
|
1799
|
+
updates.waitReason = null;
|
|
1800
|
+
}
|
|
1801
|
+
else if (result.type === "wait") {
|
|
1802
|
+
updates.waitReason = result.reason ?? null;
|
|
1803
|
+
updates.lastError = null;
|
|
1804
|
+
}
|
|
1805
|
+
else if (result.type === "input_required") {
|
|
1806
|
+
updates.waitReason = result.question ?? null;
|
|
1807
|
+
updates.lastError = null;
|
|
1808
|
+
}
|
|
1809
|
+
else {
|
|
1810
|
+
updates.waitReason = null;
|
|
1811
|
+
updates.lastError = null;
|
|
1812
|
+
}
|
|
1813
|
+
const turnEndedAt = new Date();
|
|
1814
|
+
await cmsRetryBestEffort(`runTurn.postTurn completeTurnWriteback state=${updates.state} session=${input.sessionId}`, () => catalog.completeTurnWriteback({
|
|
1815
|
+
sessionId: input.sessionId,
|
|
1816
|
+
agentId: null,
|
|
1817
|
+
model: input.config.model ?? null,
|
|
1818
|
+
reasoningEffort: input.config.reasoningEffort ?? null,
|
|
1819
|
+
turnIndex: input.turnIndex ?? 0,
|
|
1820
|
+
startedAt: turnStartedAt,
|
|
1821
|
+
endedAt: turnEndedAt,
|
|
1822
|
+
durationMs: Math.max(0, turnEndedAt.getTime() - turnStartedAt.getTime()),
|
|
1823
|
+
tokensInput: turnTelemetry.tokensInput,
|
|
1824
|
+
tokensOutput: turnTelemetry.tokensOutput,
|
|
1825
|
+
tokensCacheRead: turnTelemetry.tokensCacheRead,
|
|
1826
|
+
tokensCacheWrite: turnTelemetry.tokensCacheWrite,
|
|
1827
|
+
toolCalls: turnTelemetry.toolCalls,
|
|
1828
|
+
toolErrors: turnTelemetry.toolErrors,
|
|
1829
|
+
toolNames: Array.from(turnTelemetry.toolNames).sort(),
|
|
1830
|
+
resultType: result.type,
|
|
1831
|
+
errorMessage: result.type === "error" ? (result.message ?? null) : null,
|
|
1832
|
+
workerNodeId: workerNodeId ?? null,
|
|
1833
|
+
state: updates.state ?? "idle",
|
|
1834
|
+
lastActiveAt: updates.lastActiveAt ?? turnEndedAt,
|
|
1835
|
+
lastError: updates.lastError ?? null,
|
|
1836
|
+
waitReason: updates.waitReason ?? null,
|
|
1837
|
+
currentIteration: input.turnIndex ?? 0,
|
|
1838
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1699
1839
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1840
|
+
return result;
|
|
1841
|
+
};
|
|
1842
|
+
const bodyResult = await executeTurnBody();
|
|
1843
|
+
// ── Session lifecycle protocol commit (proposal §3.2) ───────────
|
|
1844
|
+
// The turn and its snapshot durability are one activity completion:
|
|
1845
|
+
// no observable state exists where the turn "happened" but its
|
|
1846
|
+
// snapshot doesn't. Commit on every result the body produced —
|
|
1847
|
+
// matching today's semantics where the warm session keeps whatever
|
|
1848
|
+
// state the turn (even a cancelled/errored one) left behind. The
|
|
1849
|
+
// layout guard skips paths where no snapshottable session exists:
|
|
1850
|
+
// the SDK writes workspace.yaml on session create, so a dir without
|
|
1851
|
+
// it (fake test clients, failed creates — possibly containing only
|
|
1852
|
+
// our own sentinel) has nothing the store could meaningfully hold,
|
|
1853
|
+
// and committing it would fail the whole turn.
|
|
1854
|
+
const lifecycleSessionDir = lifecycle
|
|
1855
|
+
? path.join(sessionManager.getSessionStateDir(), input.sessionId)
|
|
1856
|
+
: null;
|
|
1857
|
+
if (lifecycle && lifecycleSessionDir && fs.existsSync(path.join(lifecycleSessionDir, "workspace.yaml"))) {
|
|
1858
|
+
const committed = await runTurnCommit(lifecycle, lifecycleBaseVersion, bodyResult);
|
|
1859
|
+
if (catalog && committed.sizeBytes != null) {
|
|
1860
|
+
await cmsRetryBestEffort(`runTurn.commitSummary session=${input.sessionId}`, () => catalog.upsertSessionMetricSummary(input.sessionId, {
|
|
1861
|
+
snapshotSizeBytes: committed.sizeBytes,
|
|
1862
|
+
...(committed.rawSizeBytes != null ? { rawSizeBytes: committed.rawSizeBytes } : {}),
|
|
1863
|
+
}), (msg) => activityCtx.traceInfo(msg));
|
|
1703
1864
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1865
|
+
if (committed.alreadyCommitted && committed.storedResult !== undefined) {
|
|
1866
|
+
// A racing attempt of this same turn won the CAS. Its
|
|
1867
|
+
// snapshot was restored (restore-not-replay, §3.2 r1–r3) —
|
|
1868
|
+
// return ITS result so the recorded outcome matches the
|
|
1869
|
+
// durable lineage instead of this attempt's divergent body.
|
|
1870
|
+
activityCtx.traceInfo(`[runTurn] session=${input.sessionId} racing attempt committed v${committed.version} first; ` +
|
|
1871
|
+
`adopting its stored result`);
|
|
1872
|
+
return { ...committed.storedResult, snapshotVersion: committed.version };
|
|
1707
1873
|
}
|
|
1708
|
-
|
|
1709
|
-
await cmsRetryBestEffort(`runTurn.postTurn completeTurnWriteback state=${updates.state} session=${input.sessionId}`, () => catalog.completeTurnWriteback({
|
|
1710
|
-
sessionId: input.sessionId,
|
|
1711
|
-
agentId: null,
|
|
1712
|
-
model: input.config.model ?? null,
|
|
1713
|
-
reasoningEffort: input.config.reasoningEffort ?? null,
|
|
1714
|
-
turnIndex: input.turnIndex ?? 0,
|
|
1715
|
-
startedAt: turnStartedAt,
|
|
1716
|
-
endedAt: turnEndedAt,
|
|
1717
|
-
durationMs: Math.max(0, turnEndedAt.getTime() - turnStartedAt.getTime()),
|
|
1718
|
-
tokensInput: turnTelemetry.tokensInput,
|
|
1719
|
-
tokensOutput: turnTelemetry.tokensOutput,
|
|
1720
|
-
tokensCacheRead: turnTelemetry.tokensCacheRead,
|
|
1721
|
-
tokensCacheWrite: turnTelemetry.tokensCacheWrite,
|
|
1722
|
-
toolCalls: turnTelemetry.toolCalls,
|
|
1723
|
-
toolErrors: turnTelemetry.toolErrors,
|
|
1724
|
-
toolNames: Array.from(turnTelemetry.toolNames).sort(),
|
|
1725
|
-
resultType: result.type,
|
|
1726
|
-
errorMessage: result.type === "error" ? (result.message ?? null) : null,
|
|
1727
|
-
workerNodeId: workerNodeId ?? null,
|
|
1728
|
-
state: updates.state ?? "idle",
|
|
1729
|
-
lastActiveAt: updates.lastActiveAt ?? turnEndedAt,
|
|
1730
|
-
lastError: updates.lastError ?? null,
|
|
1731
|
-
waitReason: updates.waitReason ?? null,
|
|
1732
|
-
currentIteration: input.turnIndex ?? 0,
|
|
1733
|
-
}), (msg) => activityCtx.traceInfo(msg));
|
|
1874
|
+
return { ...bodyResult, snapshotVersion: committed.version };
|
|
1734
1875
|
}
|
|
1735
|
-
return
|
|
1876
|
+
return bodyResult;
|
|
1736
1877
|
}, { trace });
|
|
1737
1878
|
if (!finalTurnResult) {
|
|
1738
1879
|
throw new Error("runTurn completed without a turn result");
|
|
@@ -1812,6 +1953,21 @@ workerNodeId) {
|
|
|
1812
1953
|
activityCtx.traceInfo(`[abortTurn] session=${input.sessionId} outcome=${result.outcome}${result.detail ? ` (${result.detail})` : ""}`);
|
|
1813
1954
|
return result;
|
|
1814
1955
|
});
|
|
1956
|
+
// ── LEGACY LIFECYCLE ACTIVITIES (compat surface, not dead code) ──
|
|
1957
|
+
//
|
|
1958
|
+
// dehydrateSession / hydrateSession / needsHydrationSession /
|
|
1959
|
+
// checkpointSession are scheduled ONLY by frozen orchestration versions
|
|
1960
|
+
// (≤ 1.0.56: their processPrompt probes needsHydrationSession before
|
|
1961
|
+
// every turn, their wait policy dehydrates, their warm continue-as-new
|
|
1962
|
+
// checkpoints) and by 1.0.57's one-shot normalization of a legacy CAN
|
|
1963
|
+
// input (hydrateSession when input.needsHydration=true). The lifecycle
|
|
1964
|
+
// protocol itself never schedules any of them — hydrate/commit live
|
|
1965
|
+
// inside the runTurn activity (P5).
|
|
1966
|
+
//
|
|
1967
|
+
// Retirement condition: when the ≤1.0.56 handlers are dropped from
|
|
1968
|
+
// DURABLE_SESSION_ORCHESTRATION_REGISTRY, delete these registrations
|
|
1969
|
+
// (and the session-proxy methods that schedule them) in the same
|
|
1970
|
+
// change. Until then they must stay byte-compatible.
|
|
1815
1971
|
// ── dehydrateSession ────────────────────────────────────
|
|
1816
1972
|
runtime.registerActivity("dehydrateSession", async (activityCtx, input) => {
|
|
1817
1973
|
const reason = input.reason ?? "unknown";
|
|
@@ -1884,7 +2040,7 @@ workerNodeId) {
|
|
|
1884
2040
|
trace(`session=${input.sessionId} complete reason=${reason}`);
|
|
1885
2041
|
dehydrationSpan.setAttribute("pilotswarm.dehydration_result", "completed");
|
|
1886
2042
|
if (catalog) {
|
|
1887
|
-
const snapshotSizeBytes = await tryReadSnapshotSizeBytes(
|
|
2043
|
+
const snapshotSizeBytes = await tryReadSnapshotSizeBytes(sessionStore, input.sessionId);
|
|
1888
2044
|
if (snapshotSizeBytes != null) {
|
|
1889
2045
|
dehydrationSpan.setAttribute("pilotswarm.snapshot_size_bytes", snapshotSizeBytes);
|
|
1890
2046
|
}
|
|
@@ -1971,7 +2127,7 @@ workerNodeId) {
|
|
|
1971
2127
|
runtime.registerActivity("checkpointSession", async (activityCtx, input) => {
|
|
1972
2128
|
await sessionManager.checkpoint(input.sessionId);
|
|
1973
2129
|
if (catalog) {
|
|
1974
|
-
const snapshotSizeBytes = await tryReadSnapshotSizeBytes(
|
|
2130
|
+
const snapshotSizeBytes = await tryReadSnapshotSizeBytes(sessionStore, input.sessionId);
|
|
1975
2131
|
// Best-effort: metric summary is observability only. The blob has
|
|
1976
2132
|
// already been written by sessionManager.checkpoint above.
|
|
1977
2133
|
await cmsRetryBestEffort(`checkpointSession.upsertSummary session=${input.sessionId}`, () => catalog.upsertSessionMetricSummary(input.sessionId, {
|