Skip to content
Merged
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
13 changes: 13 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ export interface OrchestratorV2Shape {
Effect.Success<ReturnType<ProjectionStoreV2["Service"]["getPullRequestSnapshot"]>>,
OrchestratorV2Error
>;
readonly getAgentAwarenessSnapshot: (
threadId?: ThreadId,
) => Effect.Effect<
Effect.Success<ReturnType<ProjectionStoreV2["Service"]["getAgentAwarenessSnapshot"]>>,
OrchestratorV2Error
>;
readonly getShellSnapshot: (
options?: ShellSnapshotOptions,
) => Effect.Effect<OrchestrationV2ThreadShellSnapshot, OrchestratorV2Error>;
Expand Down Expand Up @@ -14188,6 +14194,13 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
.getPullRequestSnapshot(options)
.pipe(Effect.mapError(shellProjectionError));
}),
getAgentAwarenessSnapshot: Effect.fn("OrchestratorV2.getAgentAwarenessSnapshot")(
function (threadId) {
return projectionStore
.getAgentAwarenessSnapshot(threadId)
.pipe(Effect.mapError(shellProjectionError));
},
),
getShellSnapshot: Effect.fn("OrchestratorV2.getShellSnapshot")(function (options) {
return projectionStore.getShellSnapshot(options).pipe(Effect.mapError(shellProjectionError));
}),
Expand Down
44 changes: 43 additions & 1 deletion apps/server/src/orchestration-v2/ProjectionStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
TurnItemId,
type ThreadPullRequestLink,
isLiveSubagentShell,
isListedThreadLineage,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -96,8 +97,40 @@ const traceSqlStatements = () => {
/** The shell index flags every live shell and matches its shells on the fields subscriber trims read. */
const assertShellIndexCoversShells = Effect.fnUntraced(function* () {
const store = yield* ProjectionStore.ProjectionStoreV2;
const shells = (yield* store.getShellSnapshot({ location: "active" })).threads;
const snapshot = yield* store.getShellSnapshot();
const shells = snapshot.threads;
const index = yield* store.listActiveShellIndex();
const { tracer, statements, queryTransactions } = traceSqlStatements();
const activity = yield* store.getAgentAwarenessSnapshot().pipe(Effect.withTracer(tracer));
assert.equal(activity.snapshotSequence, snapshot.snapshotSequence);
const awareness = (thread: Parameters<typeof projectThreadAwarenessV2>[0]["thread"]) =>
projectThreadAwarenessV2({
environmentId: EnvironmentId.make("test"),
project: { title: "Test" },
thread,
});
assert.deepEqual(
activity.threads.map(awareness),
shells.filter((thread) => isListedThreadLineage(thread.lineage)).map(awareness),
);
assert.deepEqual(
activity.threads.map((thread) => [thread.id, thread.projectId, thread.latestRunCompletedAt]),
shells
.filter((thread) => isListedThreadLineage(thread.lineage))
.map((thread) => [thread.id, thread.projectId, thread.latestRunCompletedAt]),
);
assert.isFalse(statements.some((statement) => /COUNT\(\*\)|GROUP BY/i.test(statement)));
assert.isTrue(queryTransactions.every((transaction) => transaction !== null));
assert.equal(new Set(queryTransactions).size, 1);
for (const shell of [...shells, ...snapshot.archivedThreads]) {
const targeted = yield* store
.getAgentAwarenessSnapshot(shell.id)
.pipe(Effect.withTracer(tracer));
assert.deepEqual(targeted.threads.map(awareness), [awareness(shell)]);
assert.deepEqual(targeted.threads[0]?.archivedAt, shell.archivedAt);
}
assert.isFalse(statements.some((statement) => /COUNT\(\*\)|GROUP BY/i.test(statement)));

const iso = (value: DateTime.Utc | null | undefined) =>
value == null ? null : DateTime.formatIso(value);
const trimmed = (
Expand Down Expand Up @@ -250,6 +283,7 @@ it.effect("routes full projection reads to readers and retains uncommitted write
},
});
const { queries, trace } = traceSqlRouting();
yield* store.getAgentAwarenessSnapshot().pipe(trace);
const projection = yield* store.getThreadProjection(threadId).pipe(trace);
assert.equal(projection.thread.id, threadId);
assert.deepEqual(
Expand Down Expand Up @@ -309,6 +343,12 @@ it.effect("routes full projection reads to readers and retains uncommitted write
SET payload_json = json_set(payload_json, '$.title', 'Uncommitted title')
WHERE thread_id = ${threadId}
`;
assert.equal(
(yield* store.getAgentAwarenessSnapshot()).threads.find(
(thread) => thread.id === threadId,
)?.title,
"Uncommitted title",
);
assert.equal(
(yield* store.getThreadProjection(threadId)).thread.title,
"Uncommitted title",
Expand Down Expand Up @@ -8013,6 +8053,7 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => {
});
const project = { title: "Project" };
const environmentId = EnvironmentId.make("environment:held-completion");
yield* assertShellIndexCoversShells();
const held = yield* store.getThreadShell(threadId);
assert.equal(
held && projectThreadAwarenessV2({ environmentId, project, thread: held })?.phase,
Expand All @@ -8028,6 +8069,7 @@ it.layer(layerTest)("ProjectionStoreV2", (it) => {
occurredAt: releasedAt,
payload: { ...providerThread, pendingBackgroundTasks: [], updatedAt: releasedAt },
});
yield* assertShellIndexCoversShells();
const released = yield* store.getThreadShell(threadId);
const state =
released && projectThreadAwarenessV2({ environmentId, project, thread: released });
Expand Down
97 changes: 95 additions & 2 deletions apps/server/src/orchestration-v2/ProjectionStore.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ProjectThreadAwarenessV2Input } from "@t3tools/shared/agentAwareness";
import { pendingGuestMessageCount } from "@t3tools/contracts";
import { applyThreadPullRequestLinkUpdate } from "../sourceControl/threadPullRequests.ts";
import { projectTurnItemForWire } from "./WireProjection.ts";
Expand Down Expand Up @@ -491,6 +492,12 @@ export interface ProjectionStoreV2Shape {
},
ProjectionStoreV2Error
>;
readonly getAgentAwarenessSnapshot: (
threadId?: ThreadId,
) => Effect.Effect<
{ readonly snapshotSequence: number; readonly threads: ReadonlyArray<AgentAwarenessThread> },
ProjectionStoreV2Error
>;
readonly getShellSnapshot: (
options?: ShellSnapshotOptions,
) => Effect.Effect<OrchestrationV2ThreadShellSnapshot, ProjectionStoreV2Error>;
Expand Down Expand Up @@ -980,6 +987,17 @@ type ShellPendingTurnItemRow = PayloadRow & {
readonly owning_provider_instance_id: string | null;
};

export type AgentAwarenessThread = ProjectThreadAwarenessV2Input["thread"] &
Pick<
OrchestrationV2ThreadShell,
| "projectId"
| "latestRunCompletedAt"
| "archivedAt"
| "activeRunId"
| "latestRunId"
| "hasActionableProposedPlan"
>;

type ShellThreadRow = {
readonly thread_id: string;
readonly payload_json: string;
Expand Down Expand Up @@ -6077,7 +6095,8 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
familyOf,
worktreePath,
threadIds,
}: ShellSnapshotOptions = {},
listedOnly = false,
}: ShellSnapshotOptions & { readonly listedOnly?: boolean } = {},
) =>
sql<ShellThreadRow>`
SELECT
Expand Down Expand Up @@ -6259,7 +6278,12 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
candidate.ordinal DESC, candidate.run_id DESC
LIMIT 1
) AND blocked.status = 'failed'
WHERE t.deleted_at IS NULL${threadId === undefined ? sql`` : sql` AND t.thread_id = ${threadId}`}${
WHERE t.deleted_at IS NULL${
listedOnly
? sql` AND json_extract(t.payload_json, '$.lineage.relationshipToParent') IS NOT 'subagent'
AND json_extract(t.payload_json, '$.lineage.sideChat') IS NOT 1`
: sql``
}${threadId === undefined ? sql`` : sql` AND t.thread_id = ${threadId}`}${
projectId === undefined ? sql`` : sql` AND t.project_id = ${projectId}`
}${
location === "active"
Expand Down Expand Up @@ -6990,6 +7014,74 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
return { snapshotSequence: sequenceRows[0]?.snapshot_sequence ?? 0, threads };
}, Effect.mapError(shellSnapshotReadError));

const getAgentAwarenessSnapshot = Effect.fn("ProjectionStore.getAgentAwarenessSnapshot")(
function* (threadId?: ThreadId) {
const { threadRows, providerThreadRows, pendingTurnItemRows, sequenceRows } =
yield* sql.withTransaction(
Effect.gen(function* () {
const threadRows = yield* selectShellThreadRows(
threadId,
threadId === undefined ? { location: "active", listedOnly: true } : {},
);
const ids = threadRows.map((row) => ThreadId.make(row.thread_id));
const providerThreadRows =
ids.length === 0 ? [] : yield* selectShellProviderThreadRows(ids);
const pendingTurnItemRows =
ids.length === 0
? []
: yield* selectShellPendingTurnItemRows(
ids,
threadRows.flatMap((row) =>
row.active_run_id !== null && row.active_run_status === "running"
? [row.active_run_id]
: [],
),
);
const sequenceRows = yield* sql<{ readonly snapshot_sequence: number | null }>`
SELECT MAX(sequence) AS snapshot_sequence FROM orchestration_events
WHERE application_event_version = 2 AND aggregate_kind = 'thread'
`;
return { threadRows, providerThreadRows, pendingTurnItemRows, sequenceRows };
}),
);
const { providerThreadsByThreadId, pendingTurnItemsByThreadId } =
yield* pendingBackgroundDataByThreadId({ providerThreadRows, pendingTurnItemRows });
const threads = yield* Effect.forEach(threadRows, (row) =>
shellThreadStateFromRow({
row,
// Awareness never consumes timeline counts or inherited fork history.
runOrdinalsByThreadId: new Map(),
itemCountsByThreadId: new Map(),
shellItemCountsByThreadId: new Map(),
providerThreadsByThreadId,
pendingTurnItemsByThreadId,
}).pipe(
Effect.map((state): AgentAwarenessThread => ({
id: state.thread.id,
projectId: state.thread.projectId,
archivedAt: state.thread.archivedAt,
activeRunId: state.activeRunId,
latestRunId: state.latestRunId,
hasActionableProposedPlan: state.hasActionableProposedPlan,
title: state.thread.title,
lineage: state.thread.lineage,
modelSelection: state.thread.modelSelection,
settledOverride: state.thread.settledOverride,
updatedAt: state.updatedAt,
status: state.latestRunStatus,
activityRunStatus: state.activityRunStatus,
latestRunCompletedAt: state.latestRunCompletedAt,
latestRunStopRequested: state.latestRunStopRequested,
pendingRuntimeRequest: state.pendingRuntimeRequest,
pendingBackgroundTasks: state.pendingBackgroundTasks,
})),
),
);
return { snapshotSequence: sequenceRows[0]?.snapshot_sequence ?? 0, threads };
},
Effect.mapError(shellSnapshotReadError),
);

const getShellSnapshot: ProjectionStoreV2Shape["getShellSnapshot"] = (options) =>
sql
.withTransaction(readShellSnapshotRows(options))
Expand Down Expand Up @@ -7257,6 +7349,7 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =

return {
apply,
getAgentAwarenessSnapshot: readOnReader(getAgentAwarenessSnapshot),
getShellSnapshot: readOnReader(getShellSnapshot),
getPullRequestSnapshot: readOnReader(getPullRequestSnapshot),
listActiveSubagentThreadIds: readOnReader(listActiveSubagentThreadIds),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ it.effect(
getLimitRecoveryCandidates: () => Effect.die("unused getLimitRecoveryCandidates"),
getLimitRecoveryWakeAt: () => Effect.die("unused getLimitRecoveryWakeAt"),
getOpenRemoteDelegatedTasks: Effect.die("unused getOpenRemoteDelegatedTasks"),
getAgentAwarenessSnapshot: () => Effect.die("unused awareness snapshot"),
getShellSnapshot: () => Effect.die("unused getShellSnapshot"),
listActiveSubagentThreadIds: () => Effect.die("unused listActiveSubagentThreadIds"),
countThreads: () => Effect.die("unused countThreads"),
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration-v2/ThreadManagementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ export interface ThreadManagementServiceShape {
ProjectionRecords<K>,
ThreadManagementProjectionLoadError | ThreadManagementThreadNotFoundError
>;
readonly getAgentAwarenessSnapshot: Orchestrator.OrchestratorV2["Service"]["getAgentAwarenessSnapshot"];
readonly getShellSnapshot: (
options?: Pick<
ShellSnapshotOptions,
Expand Down Expand Up @@ -1520,6 +1521,7 @@ const make = Effect.gen(function* () {
getThreadTranscript,
getThreadSnapshotWindow,
getProjectThreadRecords,
getAgentAwarenessSnapshot: orchestrator.getAgentAwarenessSnapshot,
getShellSnapshot: orchestrator.getShellSnapshot,
listActiveSubagentThreadIds: orchestrator.listActiveSubagentThreadIds,
countThreads: orchestrator.countThreads,
Expand Down
Loading
Loading