@intentic/sandbox-contract 1.240.0 → 1.243.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/README.md +52 -16
- package/dist/agent-catalog.d.ts +5 -28
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +21 -157
- package/dist/agent-catalog.js.map +1 -1
- package/dist/agent-run-model.d.ts +3 -2
- package/dist/agent-run-model.d.ts.map +1 -1
- package/dist/agent-run-model.js +9 -6
- package/dist/agent-run-model.js.map +1 -1
- package/dist/agent-runtimes.d.ts +27 -0
- package/dist/agent-runtimes.d.ts.map +1 -0
- package/dist/agent-runtimes.js +113 -0
- package/dist/agent-runtimes.js.map +1 -0
- package/dist/approvals-execution.d.ts +11 -0
- package/dist/approvals-execution.d.ts.map +1 -0
- package/dist/approvals-execution.js +44 -0
- package/dist/approvals-execution.js.map +1 -0
- package/dist/arrival.d.ts +151 -0
- package/dist/arrival.d.ts.map +1 -0
- package/dist/arrival.js +56 -0
- package/dist/arrival.js.map +1 -0
- package/dist/card-status.d.ts +5 -0
- package/dist/card-status.d.ts.map +1 -0
- package/dist/card-status.js +74 -0
- package/dist/card-status.js.map +1 -0
- package/dist/chores/chores.d.ts.map +1 -1
- package/dist/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/command-classes.d.ts +0 -1
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +9 -2
- package/dist/command-classes.js.map +1 -1
- package/dist/command-run.d.ts +10 -0
- package/dist/command-run.d.ts.map +1 -0
- package/dist/command-run.js +46 -0
- package/dist/command-run.js.map +1 -0
- package/dist/contracts/agent.contract.d.ts +991 -771
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +345 -337
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +20 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/approvals.contract.d.ts +99 -0
- package/dist/contracts/approvals.contract.d.ts.map +1 -0
- package/dist/contracts/approvals.contract.js +32 -0
- package/dist/contracts/approvals.contract.js.map +1 -0
- package/dist/contracts/ci.contract.d.ts +1 -4
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +1 -9
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/claude.contract.d.ts +9 -0
- package/dist/contracts/claude.contract.d.ts.map +1 -1
- package/dist/contracts/cursor.contract.d.ts +6 -0
- package/dist/contracts/cursor.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.d.ts +63 -1
- package/dist/contracts/git.contract.d.ts.map +1 -1
- package/dist/contracts/git.contract.js +22 -4
- package/dist/contracts/git.contract.js.map +1 -1
- package/dist/contracts/grok.contract.d.ts +3 -0
- package/dist/contracts/grok.contract.d.ts.map +1 -1
- package/dist/contracts/keys.contract.d.ts +81 -0
- package/dist/contracts/keys.contract.d.ts.map +1 -0
- package/dist/contracts/keys.contract.js +51 -0
- package/dist/contracts/keys.contract.js.map +1 -0
- package/dist/contracts/panels.contract.d.ts +7 -0
- package/dist/contracts/panels.contract.d.ts.map +1 -1
- package/dist/contracts/prepush.contract.js +2 -2
- package/dist/contracts/providers.contract.d.ts +2 -0
- package/dist/contracts/providers.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +84 -86
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/safety.contract.d.ts +35 -0
- package/dist/contracts/safety.contract.d.ts.map +1 -0
- package/dist/contracts/safety.contract.js +32 -0
- package/dist/contracts/safety.contract.js.map +1 -0
- package/dist/contracts/sessions.contract.d.ts +73 -337
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +30 -32
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +139 -337
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.d.ts +31 -3
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.d.ts +29 -20
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +13 -0
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +8 -0
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/conversation-ids.d.ts +2 -0
- package/dist/conversation-ids.d.ts.map +1 -1
- package/dist/conversation-ids.js +8 -0
- package/dist/conversation-ids.js.map +1 -1
- package/dist/credential-material.d.ts +1 -0
- package/dist/credential-material.d.ts.map +1 -1
- package/dist/credential-material.js +24 -2
- package/dist/credential-material.js.map +1 -1
- package/dist/definition.d.ts +51 -93
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +6 -28
- package/dist/definition.js.map +1 -1
- package/dist/events.d.ts +1831 -2285
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +155 -55
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +1 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +1959 -1903
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/ingress-protocol.d.ts +13 -1
- package/dist/ingress-protocol.d.ts.map +1 -1
- package/dist/ingress-protocol.js +19 -0
- package/dist/ingress-protocol.js.map +1 -1
- package/dist/mentions.d.ts +3 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +13 -0
- package/dist/mentions.js.map +1 -0
- package/dist/needs-action.d.ts +7 -0
- package/dist/needs-action.d.ts.map +1 -0
- package/dist/needs-action.js +3 -0
- package/dist/needs-action.js.map +1 -0
- package/dist/overlay-lint.d.ts +7 -0
- package/dist/overlay-lint.d.ts.map +1 -0
- package/dist/overlay-lint.js +68 -0
- package/dist/overlay-lint.js.map +1 -0
- package/dist/owner-ticket.d.ts +18 -0
- package/dist/owner-ticket.d.ts.map +1 -0
- package/dist/owner-ticket.js +51 -0
- package/dist/owner-ticket.js.map +1 -0
- package/dist/plan-pools.d.ts +13 -0
- package/dist/plan-pools.d.ts.map +1 -0
- package/dist/plan-pools.js +31 -0
- package/dist/plan-pools.js.map +1 -0
- package/dist/provider-specs.d.ts +228 -0
- package/dist/provider-specs.d.ts.map +1 -0
- package/dist/provider-specs.js +124 -0
- package/dist/provider-specs.js.map +1 -0
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +2 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/runner-protocol.js +2 -2
- package/dist/runner-protocol.js.map +1 -1
- package/dist/runtime-state.d.ts +2 -2
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +49 -0
- package/dist/safety-policy.d.ts.map +1 -0
- package/dist/safety-policy.js +52 -0
- package/dist/safety-policy.js.map +1 -0
- package/dist/schemas/agent.d.ts +21 -3
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +18 -8
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +19 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +15 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/approvals.d.ts +241 -0
- package/dist/schemas/approvals.d.ts.map +1 -0
- package/dist/schemas/approvals.js +63 -0
- package/dist/schemas/approvals.js.map +1 -0
- package/dist/schemas/automations.d.ts +5 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/ci.d.ts +5 -9
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -10
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/computers.d.ts +40 -5
- package/dist/schemas/computers.d.ts.map +1 -1
- package/dist/schemas/computers.js +16 -5
- package/dist/schemas/computers.js.map +1 -1
- package/dist/schemas/environment.d.ts +11 -131
- package/dist/schemas/environment.d.ts.map +1 -1
- package/dist/schemas/environment.js +5 -42
- package/dist/schemas/environment.js.map +1 -1
- package/dist/schemas/git-history.d.ts +4 -0
- package/dist/schemas/git-history.d.ts.map +1 -1
- package/dist/schemas/git.d.ts +179 -0
- package/dist/schemas/git.d.ts.map +1 -1
- package/dist/schemas/git.js +39 -1
- package/dist/schemas/git.js.map +1 -1
- package/dist/schemas/panels.d.ts +21 -0
- package/dist/schemas/panels.d.ts.map +1 -1
- package/dist/schemas/panels.js +5 -0
- package/dist/schemas/panels.js.map +1 -1
- package/dist/schemas/plan-limits.d.ts +50 -3
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +11 -6
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +6 -0
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.d.ts +2 -1
- package/dist/schemas/provider-subscriptions.d.ts.map +1 -1
- package/dist/schemas/provider-subscriptions.js +2 -1
- package/dist/schemas/provider-subscriptions.js.map +1 -1
- package/dist/schemas/settings.d.ts +29 -16
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +13 -20
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/system.d.ts +3 -0
- package/dist/schemas/system.d.ts.map +1 -1
- package/dist/schemas/system.js +5 -1
- package/dist/schemas/system.js.map +1 -1
- package/dist/schemas/workspace-repos.d.ts +14 -0
- package/dist/schemas/workspace-repos.d.ts.map +1 -1
- package/dist/schemas/workspace-repos.js +5 -0
- package/dist/schemas/workspace-repos.js.map +1 -1
- package/dist/schemas/workspace-tree.d.ts +1 -0
- package/dist/schemas/workspace-tree.d.ts.map +1 -1
- package/dist/schemas/workspace-tree.js +3 -0
- package/dist/schemas/workspace-tree.js.map +1 -1
- package/dist/transcript-fold.d.ts +31 -0
- package/dist/transcript-fold.d.ts.map +1 -0
- package/dist/transcript-fold.js +457 -0
- package/dist/transcript-fold.js.map +1 -0
- package/dist/workspace-state.d.ts +20 -3
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +22 -2
- package/dist/workspace-state.js.map +1 -1
- package/package.json +27 -5
- package/src/agent-catalog.test.ts +1 -1
- package/src/agent-catalog.ts +76 -555
- package/src/agent-run-model.test.ts +31 -15
- package/src/agent-run-model.ts +26 -12
- package/src/agent-runtimes.ts +419 -0
- package/src/approvals-execution.ts +96 -0
- package/src/arrival.ts +160 -0
- package/src/capability-ledger.test.ts +6 -4
- package/src/card-status.ts +93 -0
- package/src/chores/chores.ts +6 -3
- package/src/command-classes.test.ts +25 -0
- package/src/command-classes.ts +47 -5
- package/src/command-run.ts +78 -0
- package/src/contract-lock.test.ts +1 -1
- package/src/contract-lock.ts +2 -2
- package/src/contracts/agents.contract.ts +35 -2
- package/src/contracts/approvals.contract.ts +37 -0
- package/src/contracts/ci.contract.ts +0 -13
- package/src/contracts/git.contract.ts +28 -3
- package/src/contracts/keys.contract.ts +79 -0
- package/src/contracts/prepush.contract.ts +3 -3
- package/src/contracts/safety.contract.ts +48 -0
- package/src/contracts/usage.contract.ts +22 -0
- package/src/conversation-ids.test.ts +31 -1
- package/src/conversation-ids.ts +37 -3
- package/src/credential-material.test.ts +69 -1
- package/src/credential-material.ts +86 -5
- package/src/definition.ts +25 -53
- package/src/events.ts +374 -173
- package/src/history-state.ts +6 -0
- package/src/index.ts +24 -5
- package/src/ingress-protocol.ts +52 -1
- package/src/mentions.test.ts +22 -0
- package/src/mentions.ts +25 -0
- package/src/needs-action.ts +14 -0
- package/src/overlay-lint.test.ts +89 -0
- package/src/overlay-lint.ts +116 -0
- package/src/owner-ticket.test.ts +54 -0
- package/src/owner-ticket.ts +102 -0
- package/src/plan-pools.test.ts +71 -0
- package/src/plan-pools.ts +92 -0
- package/src/provider-specs.test.ts +130 -0
- package/src/provider-specs.ts +352 -0
- package/src/quick-model.ts +9 -5
- package/src/runner-protocol.ts +2 -2
- package/src/runtime-state.ts +6 -6
- package/src/safety-policy.ts +165 -0
- package/src/schemas/agent.ts +62 -17
- package/src/schemas/agents.ts +71 -0
- package/src/schemas/approvals.ts +174 -0
- package/src/schemas/ci.ts +19 -26
- package/src/schemas/computers.ts +91 -15
- package/src/schemas/environment.ts +21 -100
- package/src/schemas/git.ts +121 -6
- package/src/schemas/issues.ts +1 -1
- package/src/schemas/panels.ts +20 -0
- package/src/schemas/plan-limits.ts +58 -11
- package/src/schemas/provider-subscriptions.ts +7 -2
- package/src/schemas/settings.ts +67 -93
- package/src/schemas/system.ts +12 -1
- package/src/schemas/workspace-repos.ts +9 -0
- package/src/schemas/workspace-tree.ts +11 -0
- package/src/transcript-fold.test.ts +493 -0
- package/src/transcript-fold.ts +655 -0
- package/src/workspace-state.test.ts +77 -29
- package/src/workspace-state.ts +99 -31
- package/dist/contracts/drafts.contract.d.ts +0 -57
- package/dist/contracts/drafts.contract.d.ts.map +0 -1
- package/dist/contracts/drafts.contract.js +0 -32
- package/dist/contracts/drafts.contract.js.map +0 -1
- package/dist/definition-action.d.ts +0 -7
- package/dist/definition-action.d.ts.map +0 -1
- package/dist/definition-action.js +0 -3
- package/dist/definition-action.js.map +0 -1
- package/dist/publish-drafts.d.ts +0 -7
- package/dist/publish-drafts.d.ts.map +0 -1
- package/dist/publish-drafts.js +0 -20
- package/dist/publish-drafts.js.map +0 -1
- package/dist/schemas/drafts.d.ts +0 -84
- package/dist/schemas/drafts.d.ts.map +0 -1
- package/dist/schemas/drafts.js +0 -44
- package/dist/schemas/drafts.js.map +0 -1
- package/src/contracts/drafts.contract.ts +0 -35
- package/src/definition-action.ts +0 -8
- package/src/publish-drafts.ts +0 -59
- package/src/schemas/drafts.ts +0 -108
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { STATE_DIR } from "@intentic/constants";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { AgentEvent, TranscriptPatch, TranscriptRow } from "./events.js";
|
|
4
|
+
import { applyTranscriptPatch, foldTurn, TranscriptFold, userRow } from "./transcript-fold.js";
|
|
5
|
+
|
|
6
|
+
// When the turn started: what its user row is stamped with (TranscriptRow.sentAt).
|
|
7
|
+
const SENT_AT = 1_767_225_600_000;
|
|
8
|
+
const openingOf = (prompt: string): TranscriptRow[] => [userRow(prompt, SENT_AT, [])];
|
|
9
|
+
const foldOf = (prompt: string, events: readonly AgentEvent[]): TranscriptRow[] => foldTurn(openingOf(prompt), events);
|
|
10
|
+
|
|
11
|
+
/* THE ONE FOLD, tested where it lives. Every window draws these rows and the record keeps them, so what is
|
|
12
|
+
* asserted here is what a chat shows live, what it shows reopened, and what the record holds: one set of rules,
|
|
13
|
+
* one set of tests. */
|
|
14
|
+
describe("foldTurn", () => {
|
|
15
|
+
/* The live bubble boundary: `text_end` retires the block that WROTE something, so the calls it introduced
|
|
16
|
+
* land in a fresh bubble under it, and the prose that reports them joins that same bubble. This is Claude
|
|
17
|
+
* Code's interleaving: says what it's about to do → the cards → what it found. */
|
|
18
|
+
it("retires a prose bubble at text_end so the calls it introduced land beneath it", () => {
|
|
19
|
+
const events: AgentEvent[] = [
|
|
20
|
+
{ kind: "delta", text: "I'll look" },
|
|
21
|
+
{ kind: "text_end" },
|
|
22
|
+
{ kind: "tool_call", id: "t1", name: "Read", category: "read", status: "in_progress" },
|
|
23
|
+
{ kind: "delta", text: "found it" },
|
|
24
|
+
{ kind: "text_end" },
|
|
25
|
+
{ kind: "delta", text: "and here's why" },
|
|
26
|
+
];
|
|
27
|
+
expect(foldOf("look", events).slice(1)).toEqual([
|
|
28
|
+
{ role: "assistant", text: "I'll look" },
|
|
29
|
+
{ role: "assistant", text: "found it", tools: [{ id: "t1", name: "Read", category: "read", status: "in_progress" }] },
|
|
30
|
+
{ role: "assistant", text: "and here's why" },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/* THE USER SPOKE MID-TURN, and the rows hold it where the turn took it. It closes the open bubble: what the
|
|
35
|
+
* agent says next is its answer to these words and belongs below them. */
|
|
36
|
+
it("writes a mid-turn steer down as a user row, with the answer to it beneath", () => {
|
|
37
|
+
const events: AgentEvent[] = [
|
|
38
|
+
{ kind: "delta", text: "on it" },
|
|
39
|
+
{ kind: "steer", text: "and the tests", sentAt: SENT_AT + 1000, attachments: [`${STATE_DIR}/records/artifacts/attachments/u1/spec.md`] },
|
|
40
|
+
{ kind: "delta", text: "will do" },
|
|
41
|
+
];
|
|
42
|
+
expect(foldOf("ship it", events).slice(1)).toEqual([
|
|
43
|
+
{ role: "assistant", text: "on it" },
|
|
44
|
+
{ role: "user", text: "and the tests", sentAt: SENT_AT + 1000, attachments: [".intentic/records/artifacts/attachments/u1/spec.md"] },
|
|
45
|
+
{ role: "assistant", text: "will do" },
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// A text_end on a bubble holding only cards is not a boundary: retiring there would split a card away from
|
|
50
|
+
// the prose that reports it, a shape the live stream never draws.
|
|
51
|
+
it("does not retire a bubble that has written no prose", () => {
|
|
52
|
+
const events: AgentEvent[] = [
|
|
53
|
+
{ kind: "tool_call", id: "t1", name: "Read", category: "read", status: "completed" },
|
|
54
|
+
{ kind: "text_end" },
|
|
55
|
+
{ kind: "delta", text: "that's the file" },
|
|
56
|
+
];
|
|
57
|
+
expect(foldOf("look", events).slice(1)).toEqual([
|
|
58
|
+
{ role: "assistant", text: "that's the file", tools: [{ id: "t1", name: "Read", category: "read", status: "completed" }] },
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("settles a card from an update that lands turns after its call", () => {
|
|
63
|
+
const events: AgentEvent[] = [
|
|
64
|
+
{ kind: "tool_call", id: "t1", name: "Bash", category: "execute", status: "in_progress", target: "pnpm test" },
|
|
65
|
+
{ kind: "text_end" },
|
|
66
|
+
{ kind: "delta", text: "meanwhile" },
|
|
67
|
+
{ kind: "tool_call_update", id: "t1", status: "failed", content: [{ type: "text", text: "1 failed" }] },
|
|
68
|
+
];
|
|
69
|
+
expect(foldOf("test", events).flatMap((message) => message.tools ?? [])).toEqual([
|
|
70
|
+
{ id: "t1", name: "Bash", category: "execute", status: "failed", target: "pnpm test", content: [{ type: "text", text: "1 failed" }] },
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// A card the turn died mid-call keeps `in_progress`: that is what happened, and claiming a completion it
|
|
75
|
+
// never reported would be the one thing a transcript must not invent.
|
|
76
|
+
it("leaves an unanswered call in progress", () => {
|
|
77
|
+
const events: AgentEvent[] = [{ kind: "tool_call", id: "t1", name: "Bash", category: "execute", status: "in_progress" }];
|
|
78
|
+
expect(foldOf("run", events).at(-1)?.tools?.[0]?.status).toBe("in_progress");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/* A DELEGATION: its calls and its thinking nest under the Agent card that spawned them, so a delegation
|
|
82
|
+
* reads as one unit rather than a leaf card. Its PROSE stays off the card: that card has nowhere to render
|
|
83
|
+
* prose, and the child's report already arrives as the card's own result content. The child's live state
|
|
84
|
+
* (the `subagent` frames) rides the same card. */
|
|
85
|
+
it("nests a subagent's calls, thinking and live state under the card that spawned them", () => {
|
|
86
|
+
const events: AgentEvent[] = [
|
|
87
|
+
{ kind: "delta", text: "delegating" },
|
|
88
|
+
{ kind: "tool_call", id: "task-1", name: "Agent", category: "other", status: "in_progress" },
|
|
89
|
+
{ kind: "subagent", id: "task-1", subagentKind: "subagent", agentType: "Explore", background: true },
|
|
90
|
+
{ kind: "delta", text: "inner voice", parentToolUseId: "task-1" },
|
|
91
|
+
{ kind: "thinking", text: "hmm", parentToolUseId: "task-1" },
|
|
92
|
+
{ kind: "tool_call", id: "t2", name: "Read", category: "read", status: "in_progress", parentToolUseId: "task-1" },
|
|
93
|
+
{ kind: "tool_call_update", id: "t2", status: "completed" },
|
|
94
|
+
{ kind: "subagent_update", id: "task-1", status: "completed", toolUses: 1 },
|
|
95
|
+
];
|
|
96
|
+
expect(foldOf("delegate", events).at(-1)?.tools).toEqual([
|
|
97
|
+
{
|
|
98
|
+
id: "task-1",
|
|
99
|
+
name: "Agent",
|
|
100
|
+
category: "other",
|
|
101
|
+
status: "in_progress",
|
|
102
|
+
thinking: "hmm",
|
|
103
|
+
children: [{ id: "t2", name: "Read", category: "read", status: "completed" }],
|
|
104
|
+
subagent: { kind: "subagent", agentType: "Explore", background: true, status: "completed", toolUses: 1 },
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Its Agent card is not in this stream (a malformed log, or one level of a deeper spawn read on its own), so
|
|
110
|
+
// there is nothing to hang the child off, which is exactly what keeps a nested level out of the level above.
|
|
111
|
+
it("drops a subagent's frames when the card that spawned them is absent", () => {
|
|
112
|
+
const events: AgentEvent[] = [
|
|
113
|
+
{ kind: "delta", text: "delegating" },
|
|
114
|
+
{ kind: "tool_call", id: "t2", name: "Read", category: "read", status: "completed", parentToolUseId: "task-1" },
|
|
115
|
+
];
|
|
116
|
+
expect(foldOf("delegate", events).slice(1)).toEqual([{ role: "assistant", text: "delegating" }]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// One subagent's own side of the same log: what the Subagents area renders while it runs. Read at the
|
|
120
|
+
// child's level its prose IS top-level, the parent's frames are not its business, and the parent's cards
|
|
121
|
+
// stay out: read at a subagent's own level a card is not that stream's.
|
|
122
|
+
it("reads one subagent's stream as a transcript of its own", () => {
|
|
123
|
+
const questions = [{ question: "Which?", header: "Pick", multiSelect: false, options: [{ label: "A", description: "a" }] }];
|
|
124
|
+
const events: AgentEvent[] = [
|
|
125
|
+
{ kind: "delta", text: "parent prose" },
|
|
126
|
+
{ kind: "tool_call", id: "t1", name: "Grep", category: "search", status: "completed" },
|
|
127
|
+
{ kind: "delta", text: "found it", parentToolUseId: "task-1" },
|
|
128
|
+
{ kind: "tool_call", id: "t2", name: "Read", category: "read", status: "completed", parentToolUseId: "task-1" },
|
|
129
|
+
{ kind: "question", requestId: "q1", questions },
|
|
130
|
+
];
|
|
131
|
+
expect(foldTurn([], events, "settled", "task-1")).toEqual([
|
|
132
|
+
{ role: "assistant", text: "found it", tools: [{ id: "t2", name: "Read", category: "read", status: "completed" }] },
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("records the thinking a turn showed", () => {
|
|
137
|
+
const events: AgentEvent[] = [
|
|
138
|
+
{ kind: "thinking", text: "hm, " },
|
|
139
|
+
{ kind: "thinking", text: "maybe" },
|
|
140
|
+
{ kind: "delta", text: "yes" },
|
|
141
|
+
];
|
|
142
|
+
expect(foldOf("think", events).at(-1)).toEqual({ role: "assistant", text: "yes", thinking: "hm, maybe" });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("records the task checklist a turn maintained", () => {
|
|
146
|
+
const events: AgentEvent[] = [
|
|
147
|
+
{ kind: "delta", text: "planning" },
|
|
148
|
+
{ kind: "text_end" },
|
|
149
|
+
{
|
|
150
|
+
kind: "todos",
|
|
151
|
+
items: [
|
|
152
|
+
{ content: "step 1", status: "completed" },
|
|
153
|
+
{ content: "step 2", status: "in_progress", activeForm: "Running step 2" },
|
|
154
|
+
{ content: "step 3", status: "pending" },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{ kind: "tool_call", id: "t1", name: "Bash", category: "execute", status: "in_progress" },
|
|
158
|
+
];
|
|
159
|
+
expect(foldOf("run tasks", events)).toEqual([
|
|
160
|
+
{ role: "user", text: "run tasks", sentAt: SENT_AT },
|
|
161
|
+
{ role: "assistant", text: "planning" },
|
|
162
|
+
{
|
|
163
|
+
role: "assistant",
|
|
164
|
+
text: "",
|
|
165
|
+
todos: [
|
|
166
|
+
{ content: "step 1", status: "completed" },
|
|
167
|
+
{ content: "step 2", status: "in_progress", activeForm: "Running step 2" },
|
|
168
|
+
{ content: "step 3", status: "pending" },
|
|
169
|
+
],
|
|
170
|
+
tools: [{ id: "t1", name: "Bash", category: "execute", status: "in_progress" }],
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// End-of-turn accounting lands on the bubble the answer ended in, and closes the turn: a steered
|
|
176
|
+
// conversation's stream can carry several turns, and the next opens a fresh bubble below.
|
|
177
|
+
it("attaches the turn's usage to its last bubble and closes it", () => {
|
|
178
|
+
const events: AgentEvent[] = [
|
|
179
|
+
{ kind: "delta", text: "done" },
|
|
180
|
+
{ kind: "usage", account: "a", costUsd: 0.5, inputTokens: 10, outputTokens: 5, cacheReadTokens: 3 },
|
|
181
|
+
{ kind: "delta", text: "next turn" },
|
|
182
|
+
];
|
|
183
|
+
expect(foldOf("go", events).slice(1)).toEqual([
|
|
184
|
+
{ role: "assistant", text: "done", usage: { costUsd: 0.5, inputTokens: 10, outputTokens: 5 } },
|
|
185
|
+
{ role: "assistant", text: "next turn" },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Frames that are not transcript (init, the settle) carry no bubble of their own, so a turn that only
|
|
190
|
+
// emitted those folds to the prompt alone rather than an empty reply.
|
|
191
|
+
it("yields nothing but the prompt for a turn that said nothing", () => {
|
|
192
|
+
const events: AgentEvent[] = [{ kind: "init", model: "claude-opus-4" }, { kind: "done" }];
|
|
193
|
+
expect(foldOf("hi", events)).toEqual([{ role: "user", text: "hi", sentAt: SENT_AT }]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
/* A REFUSED TURN SAYS SO. The provider's answer to this one is an error frame and no prose at all, so folding
|
|
197
|
+
* only the two speakers left a question with no reply under it. The daemon's clause about what comes next
|
|
198
|
+
* rides the same line, so the reopened chat and the live one read the same. */
|
|
199
|
+
it("keeps what went wrong, as the notice line the turn ended on", () => {
|
|
200
|
+
const refusal = "Your organization has disabled Claude subscription access for Claude Code";
|
|
201
|
+
const events: AgentEvent[] = [{ kind: "delta", text: "I'll take a look" }, { kind: "error", message: refusal }, { kind: "done" }];
|
|
202
|
+
expect(foldOf("hi", events)).toEqual([
|
|
203
|
+
{ role: "user", text: "hi", sentAt: SENT_AT },
|
|
204
|
+
{ role: "assistant", text: "I'll take a look" },
|
|
205
|
+
{ role: "notice", text: refusal },
|
|
206
|
+
]);
|
|
207
|
+
const outage: AgentEvent[] = [
|
|
208
|
+
{
|
|
209
|
+
kind: "error",
|
|
210
|
+
code: "provider-outage",
|
|
211
|
+
message: "Anthropic is down.",
|
|
212
|
+
autoResume: "scheduled",
|
|
213
|
+
outage: { retryAt: 1, attempt: 2, maxAttempts: 6 },
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
expect(foldOf("hi", outage).at(-1)).toEqual({
|
|
217
|
+
role: "notice",
|
|
218
|
+
text: "Anthropic is down. Retrying by itself: attempt 2 of 6.",
|
|
219
|
+
noticeAction: "outageOptOut",
|
|
220
|
+
});
|
|
221
|
+
const renewal: AgentEvent[] = [{ kind: "error", code: "claude-token-refused", message: "Token refused.", autoResume: "scheduled" }];
|
|
222
|
+
expect(foldOf("hi", renewal).at(-1)).toEqual({
|
|
223
|
+
role: "notice",
|
|
224
|
+
text: "Token refused. The credential is being renewed and this turn continues automatically.",
|
|
225
|
+
noticeWait: "credentialRenewal",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/* AND SO DOES A TURN THAT RAN CHEAPER THAN THE MODEL ASKED FOR. Scrolling back a week later, "was THIS
|
|
230
|
+
* answer the cheap one" is the question, and only a row per routed turn can answer it. The offer rides
|
|
231
|
+
* along so the reopened line keeps its one press. A verdict that moved nothing writes nothing. */
|
|
232
|
+
it("writes down a turn that ran on a cheaper model, and nothing for a verdict that moved nothing", () => {
|
|
233
|
+
const routed: AgentEvent[] = [
|
|
234
|
+
{ kind: "tier", tier: "fast", score: 0.1, rules: ["easy-words"], model: "claude-haiku-4-5", routed: true },
|
|
235
|
+
{ kind: "delta", text: "a closure is…" },
|
|
236
|
+
];
|
|
237
|
+
expect(foldOf("what is a closure?", routed)).toEqual([
|
|
238
|
+
{ role: "user", text: "what is a closure?", sentAt: SENT_AT },
|
|
239
|
+
{ role: "notice", text: "This turn looked simple, so it ran on claude-haiku-4-5 instead of your pick.", noticeAction: "tierHold" },
|
|
240
|
+
{ role: "assistant", text: "a closure is…" },
|
|
241
|
+
]);
|
|
242
|
+
const held: AgentEvent[] = [
|
|
243
|
+
{ kind: "tier", tier: "fast", score: 0.1, rules: ["easy-words"], model: "claude-haiku-4-5", routed: false, held: true },
|
|
244
|
+
{ kind: "delta", text: "sure" },
|
|
245
|
+
];
|
|
246
|
+
expect(foldOf("go", held).map((message) => message.role)).toEqual(["user", "assistant"]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// What happened to the turn outside the model's words: a rebase, a landing, a compaction, each one line.
|
|
250
|
+
it("writes the turn's own events down as notices", () => {
|
|
251
|
+
const events: AgentEvent[] = [
|
|
252
|
+
{ kind: "worktree", branch: "agent/x", base: "abc1234", sync: { commits: 2, blocked: [] } },
|
|
253
|
+
{ kind: "delta", text: "on it" },
|
|
254
|
+
{ kind: "compact", trigger: "auto" },
|
|
255
|
+
{ kind: "landed", landed: true, deps: { missing: 1, started: ["deps-1"], deferred: false } },
|
|
256
|
+
];
|
|
257
|
+
expect(foldOf("go", events).slice(1)).toEqual([
|
|
258
|
+
{ role: "notice", text: "Your workspace moved on while this agent waited, its branch was rebased onto your latest 2 commits." },
|
|
259
|
+
{ role: "assistant", text: "on it" },
|
|
260
|
+
{ role: "notice", text: "Context compacted to free up space." },
|
|
261
|
+
{
|
|
262
|
+
role: "notice",
|
|
263
|
+
text: "Changes landed in your workspace: review them in the Changes panel. Installing 1 new dependency it added; the project's checks run when that finishes, and the outcome lands in Activity.",
|
|
264
|
+
noticeAction: "depsInstall",
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
expect(foldOf("go", [{ kind: "landed", landed: true, held: true }]).at(-1)?.text).toBe(
|
|
268
|
+
"Finished: the work is on this agent's branch, ready to land from its review.",
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// The checkpoint and the daemon's notes land on the turn's own user row, where a reader finds them.
|
|
273
|
+
it("stamps the checkpoint and the preamble's notes on the turn's user row", () => {
|
|
274
|
+
const events: AgentEvent[] = [
|
|
275
|
+
{ kind: "checkpoint", id: "snap-1", index: 4 },
|
|
276
|
+
{ kind: "preamble", notes: [{ title: "Map of this project", text: "## Map" }] },
|
|
277
|
+
{ kind: "preamble", notes: [] },
|
|
278
|
+
{ kind: "delta", text: "on it" },
|
|
279
|
+
];
|
|
280
|
+
expect(foldOf("fix the build", events)[0]).toEqual({
|
|
281
|
+
role: "user",
|
|
282
|
+
text: "fix the build",
|
|
283
|
+
sentAt: SENT_AT,
|
|
284
|
+
checkpointId: "snap-1",
|
|
285
|
+
rewindIndex: 4,
|
|
286
|
+
notes: [{ title: "Map of this project", text: "## Map" }],
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/* THE CARD THE TURN PARKED ON, and the answer that released it. The card takes the open bubble and closes it,
|
|
291
|
+
* so the ask tool's own call (which trails its card) lands in the row beneath, with what the agent said once
|
|
292
|
+
* answered. The reply settles the card's status by the one derivation (card-status.ts). */
|
|
293
|
+
it("records the question a turn asked, with the picks that answered it, and closes the bubble on the card", () => {
|
|
294
|
+
const questions = [
|
|
295
|
+
{
|
|
296
|
+
question: "Which?",
|
|
297
|
+
header: "Pick",
|
|
298
|
+
multiSelect: true,
|
|
299
|
+
options: [
|
|
300
|
+
{ label: "A", description: "a" },
|
|
301
|
+
{ label: "B", description: "b" },
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
const events: AgentEvent[] = [
|
|
306
|
+
{ kind: "delta", text: "Two ways to go." },
|
|
307
|
+
{ kind: "text_end" },
|
|
308
|
+
{ kind: "question", requestId: "q1", questions },
|
|
309
|
+
{ kind: "tool_call", id: "t1", name: "mcp__ui__ask", category: "other", status: "in_progress" },
|
|
310
|
+
{ kind: "resolved", requestId: "q1", reply: { kind: "question", requestId: "q1", answers: { "Which?": ["A", "B"] } } },
|
|
311
|
+
{ kind: "tool_call_update", id: "t1", status: "completed" },
|
|
312
|
+
{ kind: "delta", text: "Both it is." },
|
|
313
|
+
];
|
|
314
|
+
expect(foldOf("go", events).slice(1)).toEqual([
|
|
315
|
+
{ role: "assistant", text: "Two ways to go." },
|
|
316
|
+
{ role: "assistant", text: "", question: { requestId: "q1", questions, status: "answered", answers: { "Which?": ["A", "B"] } } },
|
|
317
|
+
{ role: "assistant", text: "Both it is.", tools: [{ id: "t1", name: "mcp__ui__ask", category: "other", status: "completed" }] },
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// A card raised under prose that is still open joins that prose, the one row; the answer's continuation
|
|
322
|
+
// opens the next. A plan whose text IS the adjacent retired prose takes that row over instead of drawing
|
|
323
|
+
// the same markdown twice.
|
|
324
|
+
it("keeps the prose that led up to a card in the card's own row, and folds a repeated plan into its prose", () => {
|
|
325
|
+
const document = { path: "docs/plan.md", title: "Plan", markdown: "# Plan\n\n1. do it" };
|
|
326
|
+
const events: AgentEvent[] = [
|
|
327
|
+
{ kind: "delta", text: "Here is the plan." },
|
|
328
|
+
{ kind: "plan", requestId: "p1", text: "1. do it", document },
|
|
329
|
+
{ kind: "resolved", requestId: "p1", reply: { kind: "plan", requestId: "p1", approve: true } },
|
|
330
|
+
{ kind: "delta", text: "Doing it." },
|
|
331
|
+
];
|
|
332
|
+
expect(foldOf("plan it", events).slice(1)).toEqual([
|
|
333
|
+
{ role: "assistant", text: "Here is the plan.", plan: { requestId: "p1", text: "1. do it", document, status: "approved" } },
|
|
334
|
+
{ role: "assistant", text: "Doing it." },
|
|
335
|
+
]);
|
|
336
|
+
const repeated: AgentEvent[] = [
|
|
337
|
+
{ kind: "delta", text: "1. do it" },
|
|
338
|
+
{ kind: "text_end" },
|
|
339
|
+
{ kind: "plan", requestId: "p2", text: "1. do it" },
|
|
340
|
+
];
|
|
341
|
+
expect(foldOf("plan it", repeated).slice(1)).toEqual([
|
|
342
|
+
{ role: "assistant", text: "", plan: { requestId: "p2", text: "1. do it", status: "cancelled" } },
|
|
343
|
+
]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/* A card nobody answered is nobody's decision: when the turn ends under it, it freezes as `cancelled`, with
|
|
347
|
+
* everything it was raised with intact — including the judge's sentence, which is what the user was actually
|
|
348
|
+
* reading when the turn died. (The sentence arrives ON the card now rather than as a later frame patched
|
|
349
|
+
* into it: it is the reason the card exists, so there was no card before it existed.) */
|
|
350
|
+
it("freezes a card nobody answered as cancelled when the turn ends, keeping what it was raised with", () => {
|
|
351
|
+
const events: AgentEvent[] = [
|
|
352
|
+
{ kind: "permission", requestId: "perm1", toolName: "Bash", title: "Claude wants to run pnpm test", explain: "Runs the test suite." },
|
|
353
|
+
];
|
|
354
|
+
expect(foldOf("test", events).slice(1)).toEqual([
|
|
355
|
+
{
|
|
356
|
+
role: "assistant",
|
|
357
|
+
text: "",
|
|
358
|
+
permission: {
|
|
359
|
+
requestId: "perm1",
|
|
360
|
+
toolName: "Bash",
|
|
361
|
+
title: "Claude wants to run pnpm test",
|
|
362
|
+
explain: "Runs the test suite.",
|
|
363
|
+
status: "cancelled",
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// An offer's whole life is on its card: the click, the run showing itself living, and how the spend ended.
|
|
370
|
+
it("keeps an offer's decision, its stream and its receipt on the card that offered it", () => {
|
|
371
|
+
const offer = { slug: "research", name: "Research", publisher: "acme", description: "d", creditsPerRun: 3, request: "{}" };
|
|
372
|
+
const events: AgentEvent[] = [
|
|
373
|
+
{ kind: "service_offer", requestId: "s1", offer },
|
|
374
|
+
{ kind: "resolved", requestId: "s1", reply: { kind: "service_offer", requestId: "s1", approve: true } },
|
|
375
|
+
{ kind: "service_event", requestId: "s1", event: { event: "status", text: "searching" } },
|
|
376
|
+
{ kind: "service_receipt", requestId: "s1", outcome: "ok", credits: 3, remaining: 7 },
|
|
377
|
+
{ kind: "delta", text: "Found it." },
|
|
378
|
+
];
|
|
379
|
+
expect(foldOf("research", events).slice(1)).toEqual([
|
|
380
|
+
{
|
|
381
|
+
role: "assistant",
|
|
382
|
+
text: "",
|
|
383
|
+
serviceOffer: {
|
|
384
|
+
requestId: "s1",
|
|
385
|
+
offer,
|
|
386
|
+
status: "approved",
|
|
387
|
+
events: [{ event: "status", text: "searching" }],
|
|
388
|
+
receipt: { outcome: "ok", credits: 3, remaining: 7 },
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
{ role: "assistant", text: "Found it." },
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// A turn the user stopped says so, after freezing whatever it was parked on.
|
|
396
|
+
it("writes a stop down after cancelling what the turn was waiting on", () => {
|
|
397
|
+
const events: AgentEvent[] = [
|
|
398
|
+
{ kind: "delta", text: "half" },
|
|
399
|
+
{ kind: "question", requestId: "q1", questions: [] },
|
|
400
|
+
];
|
|
401
|
+
expect(foldTurn(openingOf("go"), events, "stopped").slice(1)).toEqual([
|
|
402
|
+
{ role: "assistant", text: "half", question: { requestId: "q1", questions: [], status: "cancelled" } },
|
|
403
|
+
{ role: "notice", text: "Stopped." },
|
|
404
|
+
]);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
/* THE PATCHES, the other half of the same fold: what a window applies to the rows it holds must reproduce the
|
|
409
|
+
* rows the fold holds, exactly, or the two drift and the reopened chat disagrees with the live one. */
|
|
410
|
+
describe("patches", () => {
|
|
411
|
+
const replay = (
|
|
412
|
+
opening: readonly TranscriptRow[],
|
|
413
|
+
events: readonly AgentEvent[],
|
|
414
|
+
): { folded: TranscriptRow[]; applied: TranscriptRow[]; patches: TranscriptPatch[] } => {
|
|
415
|
+
const fold = new TranscriptFold(opening);
|
|
416
|
+
const patches: TranscriptPatch[] = [];
|
|
417
|
+
let applied: TranscriptRow[] = [...opening];
|
|
418
|
+
for (const event of events) {
|
|
419
|
+
for (const patch of fold.apply(event)) {
|
|
420
|
+
patches.push(patch);
|
|
421
|
+
applied = applyTranscriptPatch(applied, patch);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
for (const patch of fold.finish("settled")) {
|
|
425
|
+
patches.push(patch);
|
|
426
|
+
applied = applyTranscriptPatch(applied, patch);
|
|
427
|
+
}
|
|
428
|
+
return { folded: fold.rows, applied, patches };
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
it("reproduce the fold's rows exactly, through every kind of change", () => {
|
|
432
|
+
const events: AgentEvent[] = [
|
|
433
|
+
{ kind: "checkpoint", id: "snap", index: 0 },
|
|
434
|
+
{ kind: "thinking", text: "hm" },
|
|
435
|
+
{ kind: "delta", text: "I'll " },
|
|
436
|
+
{ kind: "delta", text: "look" },
|
|
437
|
+
{ kind: "text_end" },
|
|
438
|
+
{ kind: "tool_call", id: "task-1", name: "Agent", category: "other", status: "in_progress" },
|
|
439
|
+
{ kind: "tool_call", id: "t2", name: "Read", category: "read", status: "in_progress", parentToolUseId: "task-1" },
|
|
440
|
+
{ kind: "thinking", text: "inner", parentToolUseId: "task-1" },
|
|
441
|
+
{ kind: "tool_call_update", id: "t2", status: "completed" },
|
|
442
|
+
{ kind: "todos", items: [{ content: "a", status: "pending" }] },
|
|
443
|
+
{ kind: "steer", text: "also", sentAt: SENT_AT + 1 },
|
|
444
|
+
{ kind: "delta", text: "sure" },
|
|
445
|
+
{ kind: "question", requestId: "q", questions: [] },
|
|
446
|
+
{ kind: "resolved", requestId: "q", reply: { kind: "question", requestId: "q", cancelled: true } },
|
|
447
|
+
{ kind: "usage", costUsd: 1 },
|
|
448
|
+
];
|
|
449
|
+
const { folded, applied, patches } = replay(openingOf("go"), events);
|
|
450
|
+
expect(applied).toEqual(folded);
|
|
451
|
+
expect(patches.map((patch) => patch.op)).toEqual([
|
|
452
|
+
"replace",
|
|
453
|
+
"append",
|
|
454
|
+
"thinking",
|
|
455
|
+
"text",
|
|
456
|
+
"text",
|
|
457
|
+
"append",
|
|
458
|
+
"tool",
|
|
459
|
+
"tool",
|
|
460
|
+
"tool",
|
|
461
|
+
"tool",
|
|
462
|
+
"replace",
|
|
463
|
+
"append",
|
|
464
|
+
"append",
|
|
465
|
+
"text",
|
|
466
|
+
"replace",
|
|
467
|
+
"replace",
|
|
468
|
+
"replace",
|
|
469
|
+
]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// A bubble the fold opened and never wrote into is dropped, and it was the last row, so nothing above moves.
|
|
473
|
+
it("drop an empty bubble the turn opened and abandoned", () => {
|
|
474
|
+
const events: AgentEvent[] = [
|
|
475
|
+
{ kind: "todos", items: [] },
|
|
476
|
+
{ kind: "steer", text: "hey", sentAt: SENT_AT },
|
|
477
|
+
];
|
|
478
|
+
const { folded, applied, patches } = replay(openingOf("go"), events);
|
|
479
|
+
expect(patches.map((patch) => patch.op)).toEqual(["append", "replace", "drop", "append"]);
|
|
480
|
+
expect(applied).toEqual(folded);
|
|
481
|
+
expect(folded.map((row) => row.role)).toEqual(["user", "user"]);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// A patch carries a COPY: the row the fold keeps mutating afterwards must not reach back into a patch that
|
|
485
|
+
// was already handed out, or a slow reader applies a delta on top of text that already holds it.
|
|
486
|
+
it("carry copies, not the fold's own rows", () => {
|
|
487
|
+
const fold = new TranscriptFold(openingOf("go"));
|
|
488
|
+
const [appended] = fold.apply({ kind: "delta", text: "a" });
|
|
489
|
+
fold.apply({ kind: "delta", text: "b" });
|
|
490
|
+
expect(appended).toEqual({ op: "append", row: { role: "assistant", text: "" } });
|
|
491
|
+
expect(fold.rows[1]?.text).toBe("ab");
|
|
492
|
+
});
|
|
493
|
+
});
|