Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,14 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV
prompt: update.prompt,
title: update.title,
model: update.model,
kind: "subagent",
role: { name: "general-purpose", source: "app_default" },
usage: null,
currentActivationId: null,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
result: null,
startedAt: now,
}),
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2321,6 +2321,14 @@ export function makeClaudeAdapterV2(
prompt: input.prompt ?? "",
title: input.title ?? null,
model: input.context.input.modelSelection.model,
kind: "subagent" as const,
role: { name: "general-purpose", source: "app_default" as const },
usage: null,
currentActivationId: null,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
result: null,
startedAt: now,
}),
Expand Down
18 changes: 16 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { CodexSettings, defaultInstanceIdForDriver, ProviderDriverKind } from "@t3tools/contracts";
import {
CodexSettings,
defaultInstanceIdForDriver,
orchestrationV2SubagentStatusAsTurnItemStatus,
ProviderDriverKind,
} from "@t3tools/contracts";
import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
import { resolveSpawnCommand } from "@t3tools/shared/shell";
Expand Down Expand Up @@ -100,6 +105,7 @@ import {
makeSubagentConversationArtifacts,
subagentThreadTitle,
} from "../SubagentProjection.ts";
import { defaultSubagentRole } from "../SubagentObservability.ts";

const CODEX_PROVIDER = ProviderDriverKind.make("codex");
export const CODEX_DRIVER_KIND = CODEX_PROVIDER;
Expand Down Expand Up @@ -1721,7 +1727,7 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
nativeItemRef: task.nativeTaskRef,
parentItemId: null,
ordinal: input.subagent.turnItemOrdinal,
status: task.status,
status: orchestrationV2SubagentStatusAsTurnItemStatus[task.status],
title: task.title,
startedAt: task.startedAt,
completedAt: task.completedAt,
Expand Down Expand Up @@ -1968,8 +1974,16 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi
prompt: input.prompt,
title: input.title,
model: input.model,
kind: "subagent",
role: defaultSubagentRole(),
status: "running",
result: null,
usage: null,
currentActivationId: null,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
startedAt: now,
completedAt: null,
updatedAt: now,
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,14 @@ export function makeCursorAdapterV2(
prompt: args.prompt,
title: args.description,
model: args.model ?? input.context.input.modelSelection.model,
kind: "subagent",
role: { name: "general-purpose", source: "app_default" },
usage: null,
currentActivationId: null,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
result: null,
startedAt: now,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,14 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid
prompt,
title,
model: context.model,
kind: "subagent",
role: { name: "general-purpose", source: "app_default" },
usage: null,
currentActivationId: null,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
status: subagentStatus,
result: context.result,
startedAt: context.startedAt,
Expand Down
80 changes: 80 additions & 0 deletions apps/server/src/orchestration-v2/FoundationPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ProviderThreadId,
RunAttemptId,
RunId,
SubagentActivationId,
ThreadId,
TurnItemId,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -269,13 +270,17 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
const eventSink = yield* EventSinkV2;
const projectionStore = yield* ProjectionStoreV2;
const maintenance = yield* ProjectionMaintenanceV2;
const sql = yield* SqlClient.SqlClient;
const now = yield* DateTime.now;
const parentThreadId = ThreadId.make("thread:foundation-cross-thread:parent");
const childThreadId = ThreadId.make("thread:foundation-cross-thread:child");
const childProviderThreadId = ProviderThreadId.make(
"provider-thread:foundation-cross-thread:child",
);
const subagentId = NodeId.make("subagent:foundation-cross-thread");
const activationId = SubagentActivationId.make(
"subagent:foundation-cross-thread:activation:1",
);
const spawnTransferId = ContextTransferId.make("transfer:foundation-cross-thread:spawn");
const resultTransferId = ContextTransferId.make("transfer:foundation-cross-thread:result");
const parentThread = makeThread(parentThreadId, now);
Expand Down Expand Up @@ -324,8 +329,37 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
prompt: "Inspect the child flow",
title: "Cross-thread child",
model: modelSelection.model,
kind: "subagent",
role: { name: "general-purpose", source: "app_default" },
status: "completed",
result: "done",
usage: null,
currentActivationId: null,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
startedAt: now,
completedAt: now,
updatedAt: now,
},
},
{
id: EventId.make("event:foundation-cross-thread:subagent-activation"),
type: "subagent-activation.updated",
threadId: parentThreadId,
nodeId: subagentId,
providerInstanceId,
occurredAt: now,
payload: {
id: activationId,
threadId: parentThreadId,
subagentId,
runId: null,
providerTurnId: null,
ordinal: 1,
status: "completed",
usage: { totalTokens: 240, toolUses: 2 },
startedAt: now,
completedAt: now,
updatedAt: now,
Expand Down Expand Up @@ -426,11 +460,57 @@ it.layer(TestLayer)("orchestration V2 foundation persistence", (it) => {
[childProviderThreadId],
);
assert.equal(child.thread.activeProviderThreadId, childProviderThreadId);
assert.deepEqual(
parent.subagentActivations.map((activation) => ({
id: activation.id,
totalTokens: activation.usage?.totalTokens,
})),
[
{
id: activationId,
totalTokens: 240,
},
],
);
});

yield* assertCrossThreadProjection;
assert.isTrue((yield* maintenance.verify).valid);
yield* sql`
INSERT INTO orchestration_v2_projection_subagent_activations (
activation_id,
thread_id,
subagent_id,
run_id,
provider_turn_id,
ordinal,
status,
started_at,
completed_at,
updated_at,
payload_json
)
VALUES (
'activation:foundation-cross-thread:stale',
${parentThreadId},
${subagentId},
NULL,
NULL,
99,
'completed',
NULL,
NULL,
'2026-07-26T00:00:00.000Z',
'{}'
)
`;
assert.isTrue((yield* maintenance.rebuild).valid);
const staleActivationRows = yield* sql<{ readonly count: number }>`
SELECT COUNT(*) AS count
FROM orchestration_v2_projection_subagent_activations
WHERE activation_id = 'activation:foundation-cross-thread:stale'
`;
assert.equal(staleActivationRows[0]?.count, 0);
yield* assertCrossThreadProjection;
}),
);
Expand Down
90 changes: 89 additions & 1 deletion apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type OrchestrationV2ThreadShellSnapshot,
type OrchestrationV2StoredEvent,
type OrchestrationV2Subagent,
type OrchestrationV2SubagentActivation,
type OrchestrationV2ThreadProjection,
type OrchestrationV2TurnItem,
ProviderInstanceId,
Expand Down Expand Up @@ -62,6 +63,7 @@ import {
subagentResultForRun,
subagentThreadTitle,
} from "./SubagentProjection.ts";
import { defaultSubagentRole, subagentActivationId } from "./SubagentObservability.ts";
import { ThreadForkServiceV2 } from "./ThreadForkService.ts";

export class OrchestratorDispatchError extends Schema.TaggedErrorClass<OrchestratorDispatchError>()(
Expand Down Expand Up @@ -286,7 +288,10 @@ function isTerminalDelegatedTaskStatus(status: OrchestrationV2Subagent["status"]

function delegatedTaskTerminalStatus(
status: OrchestrationV2Run["status"],
): OrchestrationV2Subagent["status"] | null {
): Extract<
OrchestrationV2Subagent["status"],
"completed" | "failed" | "cancelled" | "interrupted"
> | null {
switch (status) {
case "completed":
case "failed":
Expand All @@ -304,6 +309,12 @@ function delegatedTaskTerminalStatus(
}
}

const isTerminalSubagentActivation = (activation: OrchestrationV2SubagentActivation) =>
activation.status === "completed" ||
activation.status === "failed" ||
activation.status === "cancelled" ||
activation.status === "interrupted";

function nextQueuedRun(
projection: OrchestrationV2ThreadProjection,
): OrchestrationV2Run | undefined {
Expand Down Expand Up @@ -3849,6 +3860,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
runtimeMode: command.runtimeMode,
interactionMode: command.interactionMode,
};
const activationId = subagentActivationId(taskNodeId, 1);
const task: OrchestrationV2Subagent = {
id: taskNodeId,
threadId: command.parentThreadId,
Expand All @@ -3865,8 +3877,16 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
title: command.title ?? null,
model: command.modelSelection.model,
...(command.completionWake === undefined ? {} : { completionWake: command.completionWake }),
kind: "subagent",
role: defaultSubagentRole("delegated-worker"),
status: "running",
result: null,
usage: null,
currentActivationId: activationId,
activationCount: 1,
workflow: null,
workflowMembership: null,
recentActivity: [],
startedAt: now,
completedAt: null,
updatedAt: now,
Expand All @@ -3889,6 +3909,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
completedAt: null,
};
const parentProviderTurn = providerTurnForRun(parentProjection, parentRun);
const activation = {
id: activationId,
threadId: command.parentThreadId,
subagentId: taskNodeId,
runId: parentRun.id,
providerTurnId: parentProviderTurn?.id ?? null,
ordinal: 1,
status: "running",
usage: null,
startedAt: now,
completedAt: null,
updatedAt: now,
} satisfies OrchestrationV2SubagentActivation;
const taskTurnItem: OrchestrationV2TurnItem = {
id: taskTurnItemId,
threadId: command.parentThreadId,
Expand Down Expand Up @@ -3943,6 +3976,16 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
occurredAt: now,
payload: task,
});
yield* emitEvent({
type: "subagent-activation.updated",
threadId: command.parentThreadId,
runId: parentRun.id,
nodeId: taskNodeId,
driver: targetAdapter.driver,
providerInstanceId: command.modelSelection.instanceId,
occurredAt: now,
payload: activation,
});
yield* emitEvent({
type: "turn-item.updated",
threadId: command.parentThreadId,
Expand Down Expand Up @@ -5236,9 +5279,41 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
providerThreadId: childRun.providerThreadId,
status: terminalStatus,
result: result.text,
currentActivationId: null,
completedAt: now,
updatedAt: now,
};
const currentActivation =
task.currentActivationId === null
? undefined
: parentProjection.subagentActivations.find(
(activation) => activation.id === task.currentActivationId,
);
const terminalActivation =
task.currentActivationId === null || currentActivation?.status === terminalStatus
? null
: currentActivation === undefined
? ({
id: task.currentActivationId,
threadId: task.threadId,
subagentId: task.id,
runId: task.runId,
providerTurnId: null,
ordinal: Math.max(1, task.activationCount),
status: terminalStatus,
usage: task.usage,
startedAt: task.startedAt,
completedAt: now,
updatedAt: now,
} satisfies OrchestrationV2SubagentActivation)
: isTerminalSubagentActivation(currentActivation)
? null
: ({
...currentActivation,
status: terminalStatus,
completedAt: now,
updatedAt: now,
} satisfies OrchestrationV2SubagentActivation);
const resultTransferId = yield* idAllocator.allocate.contextTransfer({
sourceThreadId: childThreadId,
targetThreadId: parentThreadId,
Expand Down Expand Up @@ -5323,6 +5398,19 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
occurredAt: now,
payload: updatedTask,
},
...(terminalActivation === null
? []
: [
{
type: "subagent-activation.updated" as const,
threadId: parentThreadId,
...(task.runId === null ? {} : { runId: task.runId }),
nodeId: task.id,
driver: task.driver,
occurredAt: now,
payload: terminalActivation,
},
]),
...(parentNode === undefined
? []
: [
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/orchestration-v2/ProjectionMaintenance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export const layer: Layer.Layer<
yield* sql`DELETE FROM orchestration_v2_projection_provider_threads`;
yield* sql`DELETE FROM orchestration_v2_projection_provider_session_bindings`;
yield* sql`DELETE FROM orchestration_v2_projection_provider_sessions`;
yield* sql`DELETE FROM orchestration_v2_projection_subagent_activations`;
yield* sql`DELETE FROM orchestration_v2_projection_subagents`;
yield* sql`DELETE FROM orchestration_v2_projection_nodes`;
yield* sql`DELETE FROM orchestration_v2_projection_run_attempts`;
Expand Down
Loading
Loading