Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/gateway/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const GATEWAY_METHOD = {
TASKS_WORK_EARLIER: "tasks.workEarlier",
/** The page of the open task's conversation before the oldest turn held, read from the service where an older one stands; the view takes it before the rest, and the held read's cursor stays. */
TASKS_CONVERSATION_EARLIER: "tasks.conversationEarlier",
/** One page of a task's conversation as an agent's transcript is read, or a brain worker's under the call that started it, held open by the service while a turn or the worker runs; answered to the caller, never held in the view. */
/** One page of a task's conversation as an agent's transcript is read, held open by the service while a turn runs; or a brain worker's under the call that started it, read at once; answered to the caller, never held in the view. */
TASKS_MESSAGES: "tasks.messages",
/** A task's planning worker's transcript followed and let go, on the same terms as `CODING_AGENTS_TRANSCRIPT_FOLLOW`. */
TASKS_WORKER_TRANSCRIPT_FOLLOW: "tasks.workerTranscriptFollow",
Expand Down Expand Up @@ -137,7 +137,7 @@ export const GATEWAY_METHOD = {
CODING_AGENTS_WORKSPACE_LIST: "codingAgents.workspaceList",
/** A coding agent started on one task under the panel's own key; the agent, or why none started. */
CODING_AGENTS_START: "codingAgents.start",
/** One agent's transcript past a cursor, held open by the service while the agent runs. */
/** One agent's transcript past a cursor, read at once, as Copy transcript reads it; a shown tab follows it instead (`CODING_AGENTS_TRANSCRIPT_FOLLOW`). */
CODING_AGENTS_MESSAGES: "codingAgents.messages",
/**
* One agent's transcript, or one of its workers', followed from now until
Expand Down
2 changes: 1 addition & 1 deletion packages/host/src/acts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const ACT_KIND = {
TASKS_WORK_EARLIER: "tasks.workEarlier",
/** The Call tab scrolled back to its oldest turn: the open task's page of conversation before it, read from the service; the view arrives on the document. */
TASKS_CONVERSATION_EARLIER: "tasks.conversationEarlier",
/** One page of a task's conversation, or a brain worker's under the call that started it, held open by the service while it runs; answered to the tab, never on the document. */
/** One page of a task's conversation, held open by the service while it runs, or a brain worker's under the call that started it, read at once; answered to the tab, never on the document. */
TASKS_MESSAGES: "tasks.messages",
/** A File tab's window of a file of the open task's repository, read from GitHub through the service and coloured by main; answered to the tab, never on the document. */
TASKS_FILE: "tasks.file",
Expand Down
168 changes: 0 additions & 168 deletions packages/host/src/compose-coding-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,9 +945,7 @@ it.effect("each method asks the service once and answers what it said, whole", (
`list:${TASK_ID}:archived`,
"workspace:workspace-1:acme/relay",
`start:${TASK_ID}:press-1:default`,
`transcript-live:${AGENT_ID}`,
`messages-now:${AGENT_ID}:${CODING_AGENT_CURSOR_START}`,
`transcript-live:${AGENT_ID}:call-7`,
`worker-now:${AGENT_ID}:call-7:${CODING_AGENT_CURSOR_START}`,
`message:${AGENT_ID}:send-1:Also expire them after a week.`,
`choose:${AGENT_ID}:openai/gpt-6.1-sol:low`,
Expand Down Expand Up @@ -1464,172 +1462,6 @@ function windowPage(page: TranscriptNowAnswer) {
return rest;
}

it.effect(
"a running agent's transcript is held open on its Realtime subscription: the page read at once while it says something, then again only when the statements move, the empty page once the hold ends, and never a sealed statement",
() =>
Effect.gen(function* () {
const { call, asked, sealTranscript, pageNow: script, realtime } = yield* subject();
sealTranscript(SEALED_TRANSCRIPT);
const said = pageNow("1:0", { holds: false, text: "Reading the invites." });
script(said);
const first = yield* call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, {
agentId: AGENT_ID,
after: CODING_AGENT_CURSOR_START,
});
assert.deepEqual(first, windowPage(said));
yield* settled;
assert.deepEqual(realtime.opened, [SEALED_TRANSCRIPT.url]);
transcriptRows(realtime, 1);
yield* settled;

// Nothing stands past the cursor: the read is held, and the service asked nothing more until a statement moves.
const nothing = pageNow("1:0", { holds: true });
const moved = pageNow("2:0", { holds: false, text: "Writing the expiry." });
script(nothing, moved);
const next = yield* Effect.forkChild(
call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, { agentId: AGENT_ID, after: "1:0" }),
);
yield* settled;
yield* TestClock.adjust("5 seconds");
yield* settled;
assert.deepEqual(asked.filter((ask) => ask.startsWith("messages-now")).length, 2);
transcriptRows(realtime, 2);
yield* settled;
assert.deepEqual(yield* Fiber.join(next), windowPage(moved));

// Nothing moves within the hold: the page as it stands once the hold ends.
script(pageNow("2:0", { holds: true }));
const held = yield* Effect.forkChild(
call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, { agentId: AGENT_ID, after: "2:0" }),
);
yield* settled;
yield* TestClock.adjust("20 seconds");
yield* settled;
assert.deepEqual(yield* Fiber.join(held), windowPage(pageNow("2:0", { holds: true })));

assert.deepEqual(asked, [
`transcript-live:${AGENT_ID}`,
`messages-now:${AGENT_ID}:${CODING_AGENT_CURSOR_START}`,
`messages-now:${AGENT_ID}:1:0`,
`messages-now:${AGENT_ID}:1:0`,
`messages-now:${AGENT_ID}:2:0`,
`messages-now:${AGENT_ID}:2:0`,
]);
assert.deepEqual(realtime.opened, [SEALED_TRANSCRIPT.url]);
assert.ok(!JSON.stringify([first, moved]).includes("capability"));
}),
);

it.effect(
"a worker's transcript is followed under its own seal, a subscription that stops has the read read again at once and is sealed and opened again after a pause, and a sign-out lets every subscription go",
() =>
Effect.gen(function* () {
const { call, asked, sealTranscript, pageNow: script, realtime, reset } = yield* subject();
sealTranscript(SEALED_TRANSCRIPT);
const said = pageNow("2:0", { holds: false, text: "Writing the expiry." });
script(pageNow("1:0", { holds: true }), said);
const read = yield* Effect.forkChild(
call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, {
agentId: AGENT_ID,
after: CODING_AGENT_CURSOR_START,
callId: "call-7",
}),
);
yield* settled;
realtime.fail("messages");
yield* settled;
assert.deepEqual(yield* Fiber.join(read), windowPage(said));
assert.deepEqual(realtime.closed, [SEALED_TRANSCRIPT.url]);

yield* TestClock.adjust("1 second");
yield* settled;
assert.deepEqual(asked, [
`transcript-live:${AGENT_ID}:call-7`,
`worker-now:${AGENT_ID}:call-7:${CODING_AGENT_CURSOR_START}`,
`worker-now:${AGENT_ID}:call-7:${CODING_AGENT_CURSOR_START}`,
`transcript-live:${AGENT_ID}:call-7`,
]);
assert.deepEqual(realtime.opened, [SEALED_TRANSCRIPT.url, SEALED_TRANSCRIPT.url]);

yield* reset;
yield* settled;
assert.deepEqual(realtime.closed, [SEALED_TRANSCRIPT.url, SEALED_TRANSCRIPT.url]);
}),
);

it.effect(
"a transcript the account does not reach is not found, and one the service seals nothing for is sealed again each second, then read live",
() =>
Effect.gen(function* () {
const refused = yield* subject();
refused.sealTranscript({ failure: CODING_AGENT_CALL_FAILURE.NOT_FOUND });
assert.deepEqual(
yield* refused.call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, {
agentId: AGENT_ID,
after: CODING_AGENT_CURSOR_START,
}),
{ failure: CODING_AGENT_CALL_FAILURE.NOT_FOUND },
);

const unsealed = yield* subject();
const said = pageNow("1:0", { holds: false, text: "Reading the invites." });
unsealed.pageNow(said);
const read = yield* Effect.forkChild(
unsealed.call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, {
agentId: AGENT_ID,
after: CODING_AGENT_CURSOR_START,
}),
);
yield* settled;
yield* TestClock.adjust("1 second");
yield* settled;
unsealed.sealTranscript(SEALED_TRANSCRIPT);
yield* TestClock.adjust("1 second");
yield* settled;
assert.deepEqual(yield* Fiber.join(read), windowPage(said));
assert.deepEqual(unsealed.asked, [
`transcript-live:${AGENT_ID}`,
`transcript-live:${AGENT_ID}`,
`transcript-live:${AGENT_ID}`,
`messages-now:${AGENT_ID}:${CODING_AGENT_CURSOR_START}`,
]);
assert.deepEqual(unsealed.realtime.opened, [SEALED_TRANSCRIPT.url]);
}),
);

it.effect(
"a read the service says only the clock may end is read again at the edge it names, though no row moved, rather than at the hold's end",
() =>
Effect.gen(function* () {
const { call, asked, sealTranscript, pageNow: script, realtime } = yield* subject();
sealTranscript(SEALED_TRANSCRIPT);
const failed = { ...pageNow("0:0", { holds: false }), status: CODING_AGENT_STATUS.FAILED };
const starting = {
...pageNow("0:0", { holds: true }),
status: CODING_AGENT_STATUS.STARTING,
wakeIn: 3_000,
};
script(starting, starting, failed);
const read = yield* Effect.forkChild(
call(GATEWAY_METHOD.CODING_AGENTS_MESSAGES, {
agentId: AGENT_ID,
after: CODING_AGENT_CURSOR_START,
}),
);
yield* settled;
transcriptRows(realtime, 0, 0);
yield* settled;
// The first rows landing read the page again; the edge is still ahead, so it holds.
yield* TestClock.adjust("2999 millis");
yield* settled;
assert.equal(read.pollUnsafe(), undefined);
yield* TestClock.adjust("1 millis");
yield* settled;
assert.deepEqual(yield* Fiber.join(read), windowPage(failed));
assert.equal(asked.filter((ask) => ask.startsWith("messages-now")).length, 3);
}),
);

const FOLLOWED = { kind: TRANSCRIPT_KIND.AGENT, agentId: AGENT_ID } as const;

it.effect(
Expand Down
33 changes: 15 additions & 18 deletions packages/host/src/compose-coding-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,23 @@ import {
type TranscriptFollows,
TranscriptKey,
transcriptFollows,
transcriptPast,
windowsPage,
} from "./transcript-follow.js";

/**
* compose-coding-agents.ts -- a task's coding agents, asked of the service on the window's behalf: the models, the account's default, its own provider keys, and its plan template, the agents, one started, one's transcript, one messaged, one's model changed, one stopped, what one published, a clip dictated into an agent's box heard as words, the task's inbox, one item of it answered, what waits across the account, and the workspace's chat, read and written.
*
* Nothing is held here but the account inbox's follower. The window holds the agents it draws and the
* cursor it reads from, asks when a task opens, when it starts or stops an
* agent, and while an agent's tab shows its running transcript, and every
* ask is one call of the service answered whole. The transcript read is
* one held open while the agent runs and nothing new stands, here on a
* Neon Realtime subscription where the service seals one
* (`transcript-follow.ts`) and by the service where it does not, and the
* inbox read another, held while nothing in it changed, and the
* chat read a third, held while nothing in the workspace's chat moved, so each of
* the window's loops is one held read after another rather than a clock of
* its own, and a loop ends when the window stops asking. What waits across
* Nothing is held here but the account inbox's follower and the transcripts
* main follows. The window holds the agents it draws, asks when a task
* opens and when it starts or stops an agent, and every ask is one call of
* the service answered whole. A shown tab's transcript is followed rather
* than asked: main follows it for the windows drawing it, and its pages are
* told as events, read on a Neon Realtime subscription
* (`transcript-follow.ts`); a read past a cursor is answered at once, as
* Copy transcript asks it. The inbox read is held while nothing in it
* changed, and the chat read while nothing in the workspace's chat moved,
* so each of those loops is one held read after another rather than a
* clock of its own, and a loop ends when the window stops asking. What waits across
* the account is followed rather than asked: main follows it while an
* account is signed in (`agent-notices.ts`), and it is heard as events,
* over Neon Realtime (`account-inbox-follow.ts`),
Expand Down Expand Up @@ -401,12 +401,9 @@ function composed(
params,
"reading an agent's transcript names the agent and a cursor",
);
const answer = gate()
? yield* transcriptPast(transcripts, new TranscriptKey({ agentId, callId }), {
now: asked(client.messagesNow(agentId, after, callId)),
})
: UNANSWERED;
return carried<CodingAgentMessagesAnswerView>(answer);
// Note that the page is read at once, since Copy transcript waits on no hold.
const answer = yield* asked(client.messagesNow(agentId, after, callId));
return carried<CodingAgentMessagesAnswerView>(windowsPage(answer));
}),
[GATEWAY_METHOD.CODING_AGENTS_TRANSCRIPT_FOLLOW]: (params) =>
Effect.gen(function* () {
Expand Down
59 changes: 10 additions & 49 deletions packages/host/src/compose-tasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2992,61 +2992,22 @@ function workerPage(
}

it.effect(
"a planning worker's transcript is held here on its live statements: read at once, again only as they move, and answered once the service says its held read would not hold",
() =>
Effect.gen(function* () {
const service = fakeService([task(INVITES, "Teammate invitations", "# Draft", 10)]);
const { call, conversations, realtime } = yield* subject(service);
conversations.workerSealed = SEALED_WORKER;
const read = yield* Effect.forkChild(
call(GATEWAY_METHOD.TASKS_MESSAGES, { taskId: INVITES, callId: "call-0-0", after: "2:0" }),
);
yield* conversations.answerWorker(workerPage([], CODING_AGENT_STATUS.RUNNING, true));
yield* conversations.answerWorker(workerPage([], CODING_AGENT_STATUS.RUNNING, true));
yield* realtime.deliver(1);
// The same rows again move nothing; new ones read the page again.
yield* realtime.deliver(1);
yield* TestClock.adjust(Duration.seconds(5));
assert.equal(conversations.workerReads.length, 2);
yield* conversations.answerWorker(
workerPage([journal("worker-1", "Compared them.")], CODING_AGENT_STATUS.COMPLETED, false),
);
yield* realtime.deliver(2);

assert.deepEqual(
yield* Fiber.join(read),
conversationPage(
[journal("worker-1", "Compared them.")],
"3:0",
CODING_AGENT_STATUS.COMPLETED,
),
);
assert.deepEqual(conversations.workerReads, [
`${INVITES}:call-0-0:2:0`,
`${INVITES}:call-0-0:2:0`,
`${INVITES}:call-0-0:2:0`,
]);
assert.deepEqual(conversations.asks, []);
}),
);

it.effect(
"a planning worker's transcript the service seals nothing for is sealed again each second, then read live",
"a planning worker's transcript is read at once past the cursor, as Copy transcript asks it, with the service's word on holding left in the host",
() =>
Effect.gen(function* () {
const service = fakeService([task(INVITES, "Teammate invitations", "# Draft", 10)]);
const { call, conversations } = yield* subject(service);
const read = yield* Effect.forkChild(
call(GATEWAY_METHOD.TASKS_MESSAGES, { taskId: INVITES, callId: "call-0-0", after: "2:0" }),
);
yield* TestClock.adjust(Duration.seconds(1));
assert.deepEqual(conversations.workerReads, []);
conversations.workerSealed = SEALED_WORKER;
yield* TestClock.adjust(Duration.seconds(1));
const page = workerPage([], CODING_AGENT_STATUS.COMPLETED, false);
const page = workerPage([], CODING_AGENT_STATUS.RUNNING, true);
yield* conversations.answerWorker(page);
const { holds: _holds, ...answered } = page;
assert.deepEqual(yield* Fiber.join(read), answered);
assert.deepEqual(
yield* call(GATEWAY_METHOD.TASKS_MESSAGES, {
taskId: INVITES,
callId: "call-0-0",
after: "2:0",
}),
answered,
);
assert.deepEqual(conversations.workerReads, [`${INVITES}:call-0-0:2:0`]);
assert.deepEqual(conversations.asks, []);
}),
Expand Down
10 changes: 5 additions & 5 deletions packages/host/src/compose-tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ import {
followTranscript,
type TranscriptFollows,
transcriptFollows,
transcriptPast,
windowsPage,
} from "./transcript-follow.js";

/**
Expand Down Expand Up @@ -1339,19 +1339,19 @@ export const composeTasks = /* @__PURE__ */ Effect.fn("host/composeTasks")(funct
});
}
const { taskId, callId, ...page } = read.success;
// A planning worker's transcript is held here on its live statements.
// A planning worker's page is read at once, since Copy transcript waits on no hold.
const answer =
callId === undefined
? yield* served(conversations.taskMessages(taskId, page))
: yield* transcriptPast(workerTranscripts, new TaskWorkerKey({ taskId, callId }), {
now: served(
: windowsPage(
yield* served(
conversations.taskWorkerMessagesNow(
taskId,
callId,
page.after ?? CODING_AGENT_CURSOR_START,
),
),
});
);
return carried<CodingAgentMessagesAnswerView>(answer);
}),
// A planning worker's transcript is followed for main, each page told as an event.
Expand Down
Loading
Loading