@adecore/agents 0.23.1 → 0.24.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.
Files changed (153) hide show
  1. package/README.md +3 -3
  2. package/dist/chat/attachment-store.js +4 -5
  3. package/dist/chat/backend.d.ts +6 -1
  4. package/dist/chat/background-work.d.ts +1 -1
  5. package/dist/chat/background-work.js +2 -1
  6. package/dist/chat/chat-core.d.ts +53 -161
  7. package/dist/chat/chat-core.js +146 -112
  8. package/dist/chat/chat-log.js +3 -3
  9. package/dist/chat/chat-process.d.ts +21 -0
  10. package/dist/chat/chat-process.js +109 -9
  11. package/dist/chat/chat-session.d.ts +10 -17
  12. package/dist/chat/chat-session.js +141 -182
  13. package/dist/chat/chat-store.d.ts +1 -0
  14. package/dist/chat/chat-store.js +105 -1
  15. package/dist/chat/claude-backend.d.ts +4 -0
  16. package/dist/chat/claude-backend.js +51 -27
  17. package/dist/chat/claude-protocol.d.ts +12 -0
  18. package/dist/chat/claude-protocol.js +137 -7
  19. package/dist/chat/claude-title.d.ts +1 -3
  20. package/dist/chat/claude-title.js +11 -39
  21. package/dist/chat/claude-transcript.d.ts +1 -0
  22. package/dist/chat/claude-transcript.js +36 -17
  23. package/dist/chat/codex-backend.d.ts +21 -1
  24. package/dist/chat/codex-backend.js +168 -10
  25. package/dist/chat/codex-protocol.d.ts +2 -0
  26. package/dist/chat/codex-protocol.js +37 -3
  27. package/dist/chat/codex-thread.js +1 -3
  28. package/dist/chat/codex-transport.d.ts +3 -0
  29. package/dist/chat/codex-transport.js +38 -35
  30. package/dist/chat/context-breakdown.js +2 -2
  31. package/dist/chat/errors.d.ts +1 -1
  32. package/dist/chat/external-sessions.js +2 -2
  33. package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
  34. package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
  35. package/dist/chat/index.d.ts +29 -0
  36. package/dist/chat/index.js +23 -0
  37. package/dist/chat/projector.d.ts +2 -1
  38. package/dist/chat/projector.js +12 -3
  39. package/dist/chat/request-summary.js +1 -4
  40. package/dist/chat/subagent-settlement.js +1 -3
  41. package/dist/chat/thread.d.ts +3 -0
  42. package/dist/chat/thread.js +20 -3
  43. package/dist/chat/ui-queries.d.ts +2 -0
  44. package/dist/chat/ui-queries.js +40 -5
  45. package/dist/chat/ui-replies.d.ts +33 -0
  46. package/dist/chat/ui-replies.js +152 -0
  47. package/dist/context/index.d.ts +2 -0
  48. package/dist/context/index.js +3 -0
  49. package/dist/coordination/index.d.ts +15 -0
  50. package/dist/coordination/index.js +16 -0
  51. package/dist/host/handlers.js +8 -0
  52. package/dist/host/index.d.ts +7 -0
  53. package/dist/host/index.js +7 -0
  54. package/dist/index.d.ts +9 -0
  55. package/dist/index.js +10 -0
  56. package/dist/providers/accounts/status.js +1 -3
  57. package/dist/providers/claude.js +3 -1
  58. package/dist/providers/codex.js +3 -1
  59. package/dist/providers/index.d.ts +14 -0
  60. package/dist/providers/index.js +12 -0
  61. package/dist/run-process.js +54 -14
  62. package/dist/skills.d.ts +1 -0
  63. package/dist/skills.js +4 -0
  64. package/dist/tasks/task-coordinator.d.ts +4 -0
  65. package/dist/tasks/task-coordinator.js +16 -2
  66. package/dist/tasks/wiring.js +7 -1
  67. package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
  68. package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
  69. package/dist/testing/bin/fake-claude.d.ts +1 -0
  70. package/dist/testing/bin/fake-claude.js +6 -0
  71. package/dist/testing/bin/fake-codex.d.ts +1 -0
  72. package/dist/testing/bin/fake-codex.js +6 -0
  73. package/dist/{chat → testing}/fake-claude.d.ts +1 -1
  74. package/dist/{chat → testing}/fake-claude.js +0 -4
  75. package/dist/{chat → testing}/fake-cli.d.ts +1 -1
  76. package/dist/{chat → testing}/fake-codex.d.ts +1 -1
  77. package/dist/{chat → testing}/fake-codex.js +70 -8
  78. package/dist/testing/index.d.ts +6 -0
  79. package/dist/testing/index.js +7 -0
  80. package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
  81. package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
  82. package/dist/usage/exchange.d.ts +5 -3
  83. package/dist/usage/exchange.js +25 -16
  84. package/dist/usage/index-file.d.ts +1 -0
  85. package/dist/usage/index-file.js +20 -2
  86. package/dist/usage/index.d.ts +9 -0
  87. package/dist/usage/index.js +6 -0
  88. package/dist/usage/limits/normalize.js +1 -3
  89. package/dist/usage/pricing.d.ts +5 -3
  90. package/dist/usage/pricing.js +28 -19
  91. package/dist/usage/scanner.js +39 -11
  92. package/dist/usage/usage-service.js +8 -3
  93. package/package.json +72 -11
  94. package/src/chat/attachment-store.ts +4 -6
  95. package/src/chat/backend.ts +8 -0
  96. package/src/chat/background-work.ts +2 -2
  97. package/src/chat/chat-core.ts +221 -143
  98. package/src/chat/chat-log.ts +3 -3
  99. package/src/chat/chat-process.ts +128 -8
  100. package/src/chat/chat-session.ts +153 -204
  101. package/src/chat/chat-store.ts +104 -1
  102. package/src/chat/claude-backend.ts +58 -26
  103. package/src/chat/claude-protocol.ts +147 -8
  104. package/src/chat/claude-title.ts +11 -39
  105. package/src/chat/claude-transcript.ts +38 -18
  106. package/src/chat/codex-backend.ts +182 -20
  107. package/src/chat/codex-protocol.ts +40 -5
  108. package/src/chat/codex-thread.ts +1 -4
  109. package/src/chat/codex-transport.ts +45 -35
  110. package/src/chat/context-breakdown.ts +2 -2
  111. package/src/chat/errors.ts +1 -0
  112. package/src/chat/external-sessions.ts +2 -2
  113. package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
  114. package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
  115. package/src/chat/index.ts +49 -0
  116. package/src/chat/projector.ts +13 -4
  117. package/src/chat/request-summary.ts +8 -5
  118. package/src/chat/subagent-settlement.ts +1 -4
  119. package/src/chat/thread.ts +22 -3
  120. package/src/chat/ui-queries.ts +46 -5
  121. package/src/chat/ui-replies.ts +187 -0
  122. package/src/context/index.ts +26 -0
  123. package/src/coordination/index.ts +67 -0
  124. package/src/host/handlers.ts +8 -0
  125. package/src/host/index.ts +8 -0
  126. package/src/index.ts +20 -0
  127. package/src/providers/accounts/status.ts +1 -4
  128. package/src/providers/claude.ts +3 -1
  129. package/src/providers/codex.ts +3 -1
  130. package/src/providers/index.ts +15 -0
  131. package/src/run-process.ts +56 -16
  132. package/src/skills.ts +5 -0
  133. package/src/tasks/task-coordinator.ts +22 -2
  134. package/src/tasks/wiring.ts +7 -1
  135. package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
  136. package/src/testing/bin/fake-claude.ts +7 -0
  137. package/src/testing/bin/fake-codex.ts +7 -0
  138. package/src/{chat → testing}/fake-claude.ts +1 -5
  139. package/src/{chat → testing}/fake-cli.ts +1 -1
  140. package/src/{chat → testing}/fake-codex.ts +75 -6
  141. package/src/testing/index.ts +7 -0
  142. package/src/{outbox → testing}/manual-clock.ts +1 -1
  143. package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
  144. package/src/usage/exchange.ts +31 -20
  145. package/src/usage/index-file.ts +23 -2
  146. package/src/usage/index.ts +10 -0
  147. package/src/usage/limits/normalize.ts +1 -4
  148. package/src/usage/pricing.ts +34 -23
  149. package/src/usage/scanner.ts +48 -11
  150. package/src/usage/usage-service.ts +8 -3
  151. /package/dist/{chat → testing}/fake-cli.js +0 -0
  152. /package/dist/{outbox → testing}/manual-clock.js +0 -0
  153. /package/dist/{watch-test-helpers.js → testing/watch.js} +0 -0
@@ -0,0 +1,136 @@
1
+ {
2
+ "version": "Claude Code 2.1.296",
3
+ "capturedAt": "2026-10-13",
4
+ "source": "Live stream-json of `/goal` with a condition the first answer could not meet: the synthetic command output, the replayed command, the model, one Stop hook check that sent it on, the model again and the one result of the whole run. Stream events, hooks, rate limits and command lifecycle frames are left out; the result keeps only the fields the protocol reads.",
5
+ "frames": [
6
+ {
7
+ "type": "assistant",
8
+ "message": {
9
+ "id": "45a12f9c-8626-4da5-9f0f-9879c170fef2",
10
+ "model": "<synthetic>",
11
+ "role": "assistant",
12
+ "type": "message",
13
+ "content": [
14
+ {
15
+ "type": "text",
16
+ "text": "Goal set: the assistant has written the word banana in two separate turns (the first reply alone is not enough)"
17
+ }
18
+ ],
19
+ "stop_reason": "end_turn"
20
+ },
21
+ "parent_tool_use_id": null,
22
+ "local_command_source": "<local-command-stdout>Goal set: the assistant has written the word banana in two separate turns (the first reply alone is not enough)</local-command-stdout>",
23
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
24
+ "uuid": "4753b572-aaf8-43bc-b7bb-eb4030430804",
25
+ "timestamp": "2026-10-13T09:43:32.152Z",
26
+ "local_command_run": {
27
+ "command": "goal",
28
+ "args": "the assistant has written the word banana in two separate turns (the first reply alone is not enough)"
29
+ }
30
+ },
31
+ {
32
+ "type": "user",
33
+ "message": {
34
+ "role": "user",
35
+ "content": "<command-name>/goal</command-name>\n <command-message>goal</command-message>\n <command-args>the assistant has written the word banana in two separate turns (the first reply alone is not enough)</command-args>"
36
+ },
37
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
38
+ "parent_tool_use_id": null,
39
+ "uuid": "33333333-3333-4333-8333-333333333333",
40
+ "timestamp": "2026-10-13T09:43:32.152Z",
41
+ "isReplay": true
42
+ },
43
+ {
44
+ "type": "assistant",
45
+ "message": {
46
+ "id": "msg_011Cfz262XsUozZNFziQzpGU",
47
+ "model": "claude-haiku-5-5",
48
+ "role": "assistant",
49
+ "type": "message",
50
+ "content": [
51
+ {
52
+ "type": "text",
53
+ "text": "Doel gezet: ik schrijf het woord \"banana\" in twee afzonderlijke beurten. Deze beurt is de eerste.\n\nbanana"
54
+ }
55
+ ],
56
+ "stop_reason": null
57
+ },
58
+ "parent_tool_use_id": null,
59
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
60
+ "uuid": "a0d5b0da-923a-4773-b445-69e36a20e7bb",
61
+ "timestamp": "2026-10-13T09:43:36.760Z",
62
+ "user_message_uuid": "33333333-3333-4333-8333-333333333333",
63
+ "user_message_uuids": [
64
+ "33333333-3333-4333-8333-333333333333"
65
+ ]
66
+ },
67
+ {
68
+ "type": "user",
69
+ "message": {
70
+ "role": "user",
71
+ "content": [
72
+ {
73
+ "type": "text",
74
+ "text": "Stop hook feedback:\n[the assistant has written the word banana in two separate turns (the first reply alone is not enough)]: Only one assistant turn containing 'banana' is present in the transcript: the reply that says \"Deze beurt is de eerste.\" followed by \"banana\". The condition requires a second separate turn with 'banana', which has not yet occurred."
75
+ }
76
+ ]
77
+ },
78
+ "parent_tool_use_id": null,
79
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
80
+ "uuid": "74b573ca-adc9-4c37-a896-bae663eebec6",
81
+ "timestamp": "2026-10-13T09:43:38.202Z",
82
+ "isSynthetic": true
83
+ },
84
+ {
85
+ "type": "system",
86
+ "subtype": "status",
87
+ "status": "compacting",
88
+ "compact_output_chars": 0,
89
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
90
+ "uuid": "074fe795-5f3a-459b-93a7-3d8f81d280b1"
91
+ },
92
+ {
93
+ "type": "system",
94
+ "subtype": "status",
95
+ "status": null,
96
+ "compact_result": "failed",
97
+ "compact_error": "too_few_groups",
98
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
99
+ "uuid": "3ebbf7de-b187-43fd-b1ec-3c48d4c589d6"
100
+ },
101
+ {
102
+ "type": "assistant",
103
+ "message": {
104
+ "id": "msg_011Cfz26UGBv5G733tMq9wHX",
105
+ "model": "claude-haiku-5-5",
106
+ "role": "assistant",
107
+ "type": "message",
108
+ "content": [
109
+ {
110
+ "type": "text",
111
+ "text": "banana"
112
+ }
113
+ ],
114
+ "stop_reason": null
115
+ },
116
+ "parent_tool_use_id": null,
117
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
118
+ "uuid": "26b88636-94fa-441a-b072-33e8707fb68b",
119
+ "timestamp": "2026-10-13T09:43:39.450Z"
120
+ },
121
+ {
122
+ "type": "result",
123
+ "subtype": "success",
124
+ "is_error": false,
125
+ "num_turns": 2,
126
+ "stop_reason": "end_turn",
127
+ "session_id": "0d328a18-28f2-48af-af0f-017ad89c26a2",
128
+ "total_cost_usd": 0.017004325,
129
+ "result": "banana",
130
+ "uuid": "64ed92e3-abde-4aeb-b0a8-3bf75fc10a6e",
131
+ "user_message_uuids": [
132
+ "33333333-3333-4333-8333-333333333333"
133
+ ]
134
+ }
135
+ ]
136
+ }
@@ -0,0 +1,2 @@
1
+ {"parentUuid":null,"isSidechain":false,"userType":"external","entrypoint":"sdk-cli","cwd":"/work/demo","sessionId":"5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10","version":"2.1.273","gitBranch":"main","type":"user","message":{"role":"user","content":"Survey the docs folder and say what is missing."},"uuid":"m-0001","timestamp":"2026-09-16T08:58:19.900Z"}
2
+ {"parentUuid":"m-0001","isSidechain":false,"userType":"external","entrypoint":"sdk-cli","cwd":"/work/demo","sessionId":"5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10","version":"2.1.273","gitBranch":"main","type":"assistant","message":{"id":"msg_main_1","role":"assistant","content":[{"type":"tool_use","id":"toolu_01ParentAgentCall","name":"Agent","input":{"description":"Survey the docs","prompt":"Survey the docs folder and say what is missing.","subagent_type":"general-purpose"}}],"stop_reason":"tool_use"},"uuid":"m-0002","timestamp":"2026-09-16T08:58:20.500Z"}
@@ -0,0 +1,29 @@
1
+ export { AttachmentStore } from './attachment-store.ts';
2
+ export type { BackendLaunch, TurnInput, ApprovalDecision, BackendEvent, BackendHost, ChatBackend } from './backend.ts';
3
+ export { runningInBackground, type BackgroundWork } from './background-work.ts';
4
+ export { BookmarkStore, type BookmarkListener } from './bookmark-store.ts';
5
+ export { type InterruptedRun, type ChatCoreHooks, type ChatCoreOptions, ChatCore } from './chat-core.ts';
6
+ export { COMPACT_ABOVE_BYTES, type ChatLogLine } from './chat-log.ts';
7
+ export { type ChatSpawnOptions, type SpawnChatProcess, spawnChatProcess, MAX_LINE_LENGTH, LineSplitter, ChatChild, type ChatProcess, type ChatChildOptions } from './chat-process.ts';
8
+ export { type PromptNotes, type ChatReferences, type ResumeWords, ChatSession, type TurnCheckpoints, type ChatSessionOptions, type LimitResumeHooks, type ChatSendExtras, type ResumeDecision } from './chat-session.ts';
9
+ export { type ChatRecordExtras, type ChatRecord, ChatStore, type ChatSeq, type ChatStoreOptions } from './chat-store.ts';
10
+ export { suggestChatTitle, type ChatTitleInput } from './chat-title.ts';
11
+ export type { ClaudeBackendOptions } from './claude-backend.ts';
12
+ export { ClaudeTitleReader } from './claude-title.ts';
13
+ export { spokenText, claudeProjectSlug, findClaudeSession } from './claude-transcript.ts';
14
+ export type { CodexBackendOptions } from './codex-backend.ts';
15
+ export { forkThreadOnce, type ThreadItemsParams, type CodexProcessSpec, type ThreadForkParams } from './codex-thread.ts';
16
+ export type { CodexClientInfo } from './codex-transport.ts';
17
+ export type { ComposerPreference } from './composer-preferences.ts';
18
+ export { type ChatErrorCode, ChatError } from './errors.ts';
19
+ export { type SessionFs, type ExternalSession, listClaudeSessions, listCodexSessions, nodeSessionFs } from './external-sessions.ts';
20
+ export { limitedTurn, limitResumeAt } from './limit-resume.ts';
21
+ export { readableAssistantText } from './readable-text.ts';
22
+ export type { SubagentChat, SubagentReaderOptions } from './subagent-reader.ts';
23
+ export type { SubagentSettlement } from './subagent-settlement.ts';
24
+ export { searchThreadItems, type ThreadHit, type ThreadSearchOptions } from './thread-search.ts';
25
+ export { ChatThread } from './thread.ts';
26
+ export { type ChatUiSource, type ChatUiHost, ChatUiRefusal } from './ui-queries.ts';
27
+ export { visualFileName, VisualStore, type VisualInput, type VisualStoreOptions, type VisualListener } from './visual-store.ts';
28
+ export { type WakeChat, chatOpener, type ChatOpenerDeps } from './wake-chat.ts';
29
+ export { type SkillRootOptions, SkillIndex } from '../skills.ts';
@@ -0,0 +1,23 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { AttachmentStore } from './attachment-store.js';
3
+ export { runningInBackground } from './background-work.js';
4
+ export { BookmarkStore } from './bookmark-store.js';
5
+ export { ChatCore } from './chat-core.js';
6
+ export { COMPACT_ABOVE_BYTES } from './chat-log.js';
7
+ export { spawnChatProcess, MAX_LINE_LENGTH, LineSplitter, ChatChild } from './chat-process.js';
8
+ export { ChatSession } from './chat-session.js';
9
+ export { ChatStore } from './chat-store.js';
10
+ export { suggestChatTitle } from './chat-title.js';
11
+ export { ClaudeTitleReader } from './claude-title.js';
12
+ export { spokenText, claudeProjectSlug, findClaudeSession } from './claude-transcript.js';
13
+ export { forkThreadOnce } from './codex-thread.js';
14
+ export { ChatError } from './errors.js';
15
+ export { listClaudeSessions, listCodexSessions, nodeSessionFs } from './external-sessions.js';
16
+ export { limitedTurn, limitResumeAt } from './limit-resume.js';
17
+ export { readableAssistantText } from './readable-text.js';
18
+ export { searchThreadItems } from './thread-search.js';
19
+ export { ChatThread } from './thread.js';
20
+ export { ChatUiRefusal } from './ui-queries.js';
21
+ export { visualFileName, VisualStore } from './visual-store.js';
22
+ export { chatOpener } from './wake-chat.js';
23
+ export { SkillIndex } from '../skills.js';
@@ -1,4 +1,4 @@
1
- import type { ChatEvent, ChatSubagentUsage } from '@adecore/agent-contracts';
1
+ import type { ChatEvent, ChatItem, ChatSubagentUsage } from '@adecore/agent-contracts';
2
2
  import type { BackendEvent } from './backend.ts';
3
3
  import type { ChatThread } from './thread.ts';
4
4
  export declare function isMainAgentOutput(event: BackendEvent): boolean;
@@ -29,6 +29,7 @@ export declare class ThreadProjector {
29
29
  private settleOpenItems;
30
30
  private outlivesTurn;
31
31
  private restatesCall;
32
+ itemOf(generation: number, ref: string): ChatItem | undefined;
32
33
  private itemId;
33
34
  private appendThinking;
34
35
  private closeThinking;
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '@adecore/agent-contracts';
1
2
  import { clipText } from '@adecore/agent-contracts/text';
2
3
  import { estimateContextBreakdown } from './context-breakdown.js';
3
4
  /*
@@ -25,9 +26,6 @@ export function isMainAgentOutput(event) {
25
26
  function startsAgentTurn(event) {
26
27
  return isMainAgentOutput(event) || ((event.type === 'approval.requested' || event.type === 'question.requested') && event.background !== true);
27
28
  }
28
- function isRecord(value) {
29
- return typeof value === 'object' && value !== null && !Array.isArray(value);
30
- }
31
29
  function str(value) {
32
30
  return typeof value === 'string' ? value : null;
33
31
  }
@@ -333,6 +331,13 @@ export class ThreadProjector {
333
331
  case 'model':
334
332
  events.push(this.thread.patchInfo({ model: event.model }));
335
333
  break;
334
+ case 'goal': {
335
+ const goal = event.goal ?? undefined;
336
+ if (JSON.stringify(goal) !== JSON.stringify(info.goal)) {
337
+ events.push(this.thread.patchInfo({ goal }));
338
+ }
339
+ break;
340
+ }
336
341
  case 'note':
337
342
  events.push(this.note(event.level, event.text));
338
343
  break;
@@ -417,6 +422,10 @@ export class ThreadProjector {
417
422
  restatesCall(generation, event) {
418
423
  return event.type === 'tool.started' && this.thread.get(this.itemId(generation, event.ref)) !== undefined;
419
424
  }
425
+ /* The item a ref of that generation names, without a walk through the thread. */
426
+ itemOf(generation, ref) {
427
+ return this.thread.get(this.itemId(generation, ref));
428
+ }
420
429
  itemId(generation, ref) {
421
430
  return `${generation}:${ref}`;
422
431
  }
@@ -1,7 +1,4 @@
1
- import { CHAT_REQUEST_LIMITS } from '@adecore/agent-contracts';
2
- function isRecord(value) {
3
- return typeof value === 'object' && value !== null && !Array.isArray(value);
4
- }
1
+ import { isRecord, CHAT_REQUEST_LIMITS } from '@adecore/agent-contracts';
5
2
  function str(value) {
6
3
  return typeof value === 'string' ? value : null;
7
4
  }
@@ -1,9 +1,7 @@
1
1
  import { open, stat } from 'node:fs/promises';
2
+ import { isRecord } from '@adecore/agent-contracts';
2
3
  // The end of a transcript is all the question needs; a final line longer than this reads as undecided.
3
4
  const TAIL_BYTES = 1024 * 1024;
4
- function isRecord(value) {
5
- return typeof value === 'object' && value !== null && !Array.isArray(value);
6
- }
7
5
  /* The report an agent handed back with `SubagentHandback` before its last message, which then only points at it. */
8
6
  function handbackBefore(lines, end) {
9
7
  for (let i = end - 1; i >= 0; i--) {
@@ -5,6 +5,7 @@ export declare class ChatThread {
5
5
  private readonly order;
6
6
  private historyGeneration;
7
7
  private readonly indices;
8
+ private readonly holds;
8
9
  constructor(info: ChatInfo, items?: ChatItem[]);
9
10
  get(id: string): ChatItem | undefined;
10
11
  list(): ChatItem[];
@@ -16,6 +17,8 @@ export declare class ChatThread {
16
17
  upsert(item: ChatItem): ChatEvent;
17
18
  appendText(itemId: string, text: string): ChatEvent | null;
18
19
  patchInfo(patch: Partial<ChatInfo>): ChatEvent;
20
+ hold(key: string): boolean;
21
+ release(key: string): boolean;
19
22
  reset(patch: Partial<ChatInfo>): ChatEvent;
20
23
  apply(event: ChatEvent): void;
21
24
  setStatus(status: AgentStatus): ChatEvent;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { appendToolOutput } from '@adecore/agent-contracts/text';
2
3
  import { ChatError } from './errors.js';
3
4
  import { requestSummaries } from './request-summary.js';
4
5
  /*
@@ -11,6 +12,8 @@ export class ChatThread {
11
12
  order = [];
12
13
  historyGeneration = randomUUID();
13
14
  indices = new Map();
15
+ // What the host asks a person beside the CLI's own requests, such as a card a verb waits on.
16
+ holds = new Set();
14
17
  constructor(info, items = []) {
15
18
  this.info = info;
16
19
  for (const item of items) {
@@ -55,15 +58,29 @@ export class ChatThread {
55
58
  }
56
59
  if (item?.kind === 'tool' && item.state === 'running') {
57
60
  const progress = item.progress ?? { startedAt: null, description: null, output: null };
58
- this.items.set(itemId, { ...item, progress: { ...progress, output: (progress.output ?? '') + text } });
61
+ this.items.set(itemId, { ...item, progress: { ...progress, output: appendToolOutput(progress.output, text) } });
59
62
  return { type: 'delta', itemId, text };
60
63
  }
61
64
  return null;
62
65
  }
66
+ /* While the host holds the chat for a person, a turn that runs reads as waiting on them. */
63
67
  patchInfo(patch) {
64
- this.info = { ...this.info, ...patch };
68
+ const held = patch.status === 'running' && this.holds.size > 0 ? { ...patch, status: 'needs-you' } : patch;
69
+ this.info = { ...this.info, ...held };
65
70
  return { type: 'info', info: this.info };
66
71
  }
72
+ /* False when the key held the chat already. */
73
+ hold(key) {
74
+ if (this.holds.has(key)) {
75
+ return false;
76
+ }
77
+ this.holds.add(key);
78
+ return true;
79
+ }
80
+ /* False when the key did not hold the chat. */
81
+ release(key) {
82
+ return this.holds.delete(key);
83
+ }
67
84
  /* Empties the thread; the info carries on with the patch on top. */
68
85
  reset(patch) {
69
86
  this.items.clear();
@@ -105,7 +122,7 @@ export class ChatThread {
105
122
  }
106
123
  /* A request can outlive the turn it came in, when work the CLI runs beside its turns asked it. */
107
124
  statusFor(activeTurnId) {
108
- if (this.pending().length > 0) {
125
+ if (this.pending().length > 0 || this.holds.size > 0) {
109
126
  return 'needs-you';
110
127
  }
111
128
  return activeTurnId === null ? 'idle' : 'running';
@@ -28,6 +28,7 @@ export declare class ChatUiQueries {
28
28
  private readonly freezing;
29
29
  private readonly cache;
30
30
  private readonly latest;
31
+ private readonly newest;
31
32
  private readonly reading;
32
33
  private readonly attempts;
33
34
  private readonly active;
@@ -43,6 +44,7 @@ export declare class ChatUiQueries {
43
44
  link(session: ChatSession, payload: ChatUiLinkPayload): Promise<ChatUiLinkReading>;
44
45
  forget(chatId: string): void;
45
46
  private evict;
47
+ private heldValues;
46
48
  private linkReading;
47
49
  private failure;
48
50
  }
@@ -35,6 +35,8 @@ export class ChatUiQueries {
35
35
  cache = new Map();
36
36
  // The read id of the newest reading per cache key, which a remount or a second client is answered with.
37
37
  latest = new Map();
38
+ // The read id of the newest reading per query identity, whose options are the ones a person picks from.
39
+ newest = new Map();
38
40
  reading = new Map();
39
41
  attempts = new Map();
40
42
  active = new Map();
@@ -187,7 +189,7 @@ export class ChatUiQueries {
187
189
  let authorAccess;
188
190
  try {
189
191
  ({ source, block, item } = this.stored(session, payload));
190
- args = uiQueryArguments(block, payload.query, payload.values);
192
+ args = uiQueryArguments(block, payload.query, payload.values, {}, this.heldValues(session, item, block));
191
193
  args = source.args.parse(args);
192
194
  authorAccess = await this.access(session, item);
193
195
  if (authorAccess === null) {
@@ -239,8 +241,9 @@ export class ChatUiQueries {
239
241
  throw new UiQueryError('refused-query', 'result-too-large', 'The UI query result is too large.');
240
242
  }
241
243
  const reading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
242
- this.cache.set(reading.readId, { key, chatId: session.id, reading });
244
+ this.cache.set(reading.readId, { key, identity, chatId: session.id, reading });
243
245
  this.latest.set(key, reading.readId);
246
+ this.newest.set(identity, reading.readId);
244
247
  while (this.cache.size > 128) {
245
248
  this.evict(this.cache.keys().next().value);
246
249
  }
@@ -283,14 +286,29 @@ export class ChatUiQueries {
283
286
  if (access === null) {
284
287
  throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
285
288
  }
289
+ const frozen = item.uiQueries?.blocks[block.id];
290
+ const readIdOf = (name) => payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
291
+ // What the person saw, by the read ids it names; each one is checked against its arguments below.
292
+ const seen = Object.create(null);
293
+ for (const name of names) {
294
+ const readId = readIdOf(name);
295
+ const cached = readId === undefined ? undefined : this.cache.get(readId);
296
+ const reading = cached?.identity === JSON.stringify([session.id, item.id, block.id, block.revision, name])
297
+ ? cached.reading
298
+ : frozen?.revision === block.revision
299
+ ? frozen?.readings[name]
300
+ : undefined;
301
+ if (readId !== undefined && reading?.readId === readId && reading.state === 'fresh') {
302
+ seen[name] = reading.value;
303
+ }
304
+ }
286
305
  for (const name of names) {
287
306
  const { source } = this.stored(session, { ...payload, query: name });
288
- const args = source.args.parse(uiQueryArguments(block, name, payload.values));
307
+ const args = source.args.parse(uiQueryArguments(block, name, payload.values, {}, seen));
289
308
  await source.authorize(session.info, access, args);
290
- const readId = payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
309
+ const readId = readIdOf(name);
291
310
  const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
292
311
  const cached = readId === undefined ? undefined : this.cache.get(readId);
293
- const frozen = item.uiQueries?.blocks[block.id];
294
312
  const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
295
313
  const reading = readId === undefined
296
314
  ? undefined
@@ -343,6 +361,23 @@ export class ChatUiQueries {
343
361
  if (cached && this.latest.get(cached.key) === readId) {
344
362
  this.latest.delete(cached.key);
345
363
  }
364
+ if (cached && this.newest.get(cached.identity) === readId) {
365
+ this.newest.delete(cached.identity);
366
+ }
367
+ }
368
+ /* The newest fresh value of every query of a block, or the frozen one, for checking an input chosen from what they read. */
369
+ heldValues(session, item, block) {
370
+ const frozen = item.uiQueries?.blocks[block.id];
371
+ const values = Object.create(null);
372
+ for (const name of Object.keys(block.queries)) {
373
+ const readId = this.newest.get(JSON.stringify([session.id, item.id, block.id, block.revision, name]));
374
+ const reading = (readId === undefined ? undefined : this.cache.get(readId)?.reading) ??
375
+ (frozen?.revision === block.revision ? frozen?.readings[name] : undefined);
376
+ if (reading?.state === 'fresh') {
377
+ values[name] = reading.value;
378
+ }
379
+ }
380
+ return values;
346
381
  }
347
382
  linkReading(value) {
348
383
  const reading = UiLinkResolutionSchema.parse(copyUiValue(value, new UiBudget()));
@@ -0,0 +1,33 @@
1
+ import { type ChatAssistantItem, type ChatEvent, type ChatItem } from '@adecore/agent-contracts';
2
+ import type { UiCompileOptions } from '@adecore/intelligent-ui/compiler';
3
+ import type { OutboxClock } from '../outbox/outbox-worker.ts';
4
+ import type { ChatThread } from './thread.ts';
5
+ export interface UiReplyHost {
6
+ schemas: UiCompileOptions['querySchemas'];
7
+ observe(item: ChatAssistantItem, final: boolean): void;
8
+ }
9
+ export interface UiRepliesOptions {
10
+ thread: ChatThread;
11
+ fenceLanguage: string;
12
+ clock: OutboxClock;
13
+ host?: UiReplyHost;
14
+ emit(event: ChatEvent): void;
15
+ repair(note: string): void;
16
+ }
17
+ export declare class UiReplies {
18
+ private readonly options;
19
+ private readonly streams;
20
+ private readonly compiled;
21
+ private readonly scanned;
22
+ private readonly referencing;
23
+ private readonly attachments;
24
+ constructor(options: UiRepliesOptions);
25
+ previews(): ChatEvent[];
26
+ previewItems(items: ChatItem[]): ChatItem[];
27
+ pass(event: ChatEvent): void;
28
+ clear(): void;
29
+ private unscannedText;
30
+ private forgetScan;
31
+ private latestAttachment;
32
+ private findAttachment;
33
+ }
@@ -0,0 +1,152 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { GeneratedImageInputSchema } from '@adecore/agent-contracts';
3
+ import { uiHasFence, uiMayReferenceHost } from '@adecore/intelligent-ui';
4
+ import { UiStream } from '@adecore/intelligent-ui/stream';
5
+ // Longer than any fence line or the name of a tag a scan looks for, so a match never falls between two deltas.
6
+ const UI_SCAN_OVERLAP = 64;
7
+ /*
8
+ * The UI blocks in the replies of a chat's main agent: every event of the chat passes through here on
9
+ * its way out, and a reply with a UI fence in it is compiled as it streams, with a preview per delta
10
+ * and the finished blocks on the item once it settles.
11
+ */
12
+ export class UiReplies {
13
+ options;
14
+ streams = new Map();
15
+ compiled = new Map();
16
+ // How much of a streaming reply's text the fence and host scans already covered, so a delta scans only its tail.
17
+ scanned = new Map();
18
+ referencing = new Set();
19
+ attachments = new Map();
20
+ constructor(options) {
21
+ this.options = options;
22
+ }
23
+ /* The preview of every reply still streaming, as deltas, for a client that attached in the middle of one. */
24
+ previews() {
25
+ const events = [];
26
+ for (const [itemId, preview] of this.compiled) {
27
+ const item = this.options.thread.get(itemId);
28
+ if (item?.kind === 'assistant' && item.streaming && item.text.length === preview.textLength) {
29
+ events.push({ type: 'delta', itemId, text: '', ui: preview.blocks, textLength: preview.textLength });
30
+ }
31
+ }
32
+ return events;
33
+ }
34
+ previewItems(items) {
35
+ return items.map((item) => {
36
+ const preview = this.compiled.get(item.id);
37
+ return item.kind === 'assistant' && preview?.textLength === item.text.length ? { ...item, ui: preview.blocks } : item;
38
+ });
39
+ }
40
+ /* Hands one event of the chat on, with the blocks of a reply it finished in place of its fences. */
41
+ pass(event) {
42
+ const { thread, emit } = this.options;
43
+ if (event.type === 'reset') {
44
+ this.clear();
45
+ }
46
+ if (event.type === 'item' && event.item.kind === 'tool') {
47
+ this.attachments.delete(event.item.turnId);
48
+ }
49
+ const item = event.type === 'item' ? event.item : event.type === 'delta' ? thread.get(event.itemId) : null;
50
+ if (item?.kind !== 'assistant' || item.parentToolUseId) {
51
+ emit(event);
52
+ return;
53
+ }
54
+ const tail = this.unscannedText(item, event.type === 'item');
55
+ if (uiMayReferenceHost(tail)) {
56
+ this.referencing.add(item.id);
57
+ }
58
+ let stream = this.streams.get(item.id);
59
+ if (!stream && !uiHasFence(tail, this.options.fenceLanguage)) {
60
+ this.forgetScan(item);
61
+ emit(event);
62
+ return;
63
+ }
64
+ if (!stream) {
65
+ stream = new UiStream({
66
+ querySchemas: this.options.host?.schemas,
67
+ fenceLanguage: this.options.fenceLanguage,
68
+ id: item.id,
69
+ clock: this.options.clock,
70
+ emit: (preview) => {
71
+ this.compiled.set(item.id, preview);
72
+ emit({ type: 'delta', itemId: item.id, text: '', ui: preview.blocks, textLength: preview.textLength });
73
+ }
74
+ });
75
+ this.streams.set(item.id, stream);
76
+ }
77
+ if (this.referencing.has(item.id)) {
78
+ this.options.host?.observe(item, false);
79
+ }
80
+ this.forgetScan(item);
81
+ const latestAttachment = this.latestAttachment(item.turnId);
82
+ if (event.type !== 'item' || item.streaming) {
83
+ emit(event);
84
+ stream.update(item.text, latestAttachment);
85
+ return;
86
+ }
87
+ const ui = stream
88
+ .finish(item.text, latestAttachment)
89
+ .map((block) => ({ ...block, revision: createHash('sha256').update(JSON.stringify(block)).digest('hex') }));
90
+ this.streams.delete(item.id);
91
+ this.compiled.delete(item.id);
92
+ const diagnostics = [...new Set(ui.flatMap((block) => block.diagnostics.map((diagnostic) => diagnostic.message)))].slice(0, 6);
93
+ if (diagnostics.length > 0) {
94
+ this.options.repair(`Your last ${this.options.fenceLanguage} block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(0, 1200));
95
+ }
96
+ const current = thread.get(item.id);
97
+ const completed = { ...item, ui, ...(current?.kind === 'assistant' && current.uiQueries ? { uiQueries: current.uiQueries } : {}) };
98
+ emit(thread.upsert(completed));
99
+ this.options.host?.observe(completed, true);
100
+ }
101
+ clear() {
102
+ for (const stream of this.streams.values()) {
103
+ stream.dispose();
104
+ }
105
+ this.streams.clear();
106
+ this.compiled.clear();
107
+ this.scanned.clear();
108
+ this.referencing.clear();
109
+ this.attachments.clear();
110
+ }
111
+ /*
112
+ * The text a scan has not covered yet, from the start of the line the last scan ended in, since a fence
113
+ * is one line; a long line is cut short, behind a character no fence or tag can follow.
114
+ */
115
+ unscannedText(item, whole) {
116
+ const scanned = whole ? 0 : (this.scanned.get(item.id) ?? 0);
117
+ this.scanned.set(item.id, item.text.length);
118
+ if (scanned === 0 || scanned > item.text.length) {
119
+ return item.text;
120
+ }
121
+ const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
122
+ const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
123
+ return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
124
+ }
125
+ forgetScan(item) {
126
+ if (!item.streaming) {
127
+ this.scanned.delete(item.id);
128
+ this.referencing.delete(item.id);
129
+ }
130
+ }
131
+ /* Looked up once per turn, and again only after one of its tool calls changed. */
132
+ latestAttachment(turnId) {
133
+ if (this.attachments.has(turnId)) {
134
+ return this.attachments.get(turnId);
135
+ }
136
+ const attachment = this.findAttachment(turnId);
137
+ this.attachments.set(turnId, attachment);
138
+ return attachment;
139
+ }
140
+ findAttachment(turnId) {
141
+ for (const item of this.options.thread.list().toReversed()) {
142
+ if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
143
+ continue;
144
+ }
145
+ const input = GeneratedImageInputSchema.safeParse(item.input);
146
+ if (input.success && input.data.attachment?.mime.startsWith('image/')) {
147
+ return input.data.attachment.id;
148
+ }
149
+ }
150
+ return undefined;
151
+ }
152
+ }
@@ -0,0 +1,2 @@
1
+ export { field, refusalBody, refusalRows, type ParsedRefusal, parseRefusalBody } from './refusal.ts';
2
+ export { VerbRefusal, type VerbCallBase, type VerbHelp, type Verb, type Action, type Noun, type ContextVerb, type VerbEntry, DRY_RUN_FLAG, REVISION_FLAG, DRY_RUN_PREVIEW, summaryLines, createVerbRegistry, lengthOf, requiredField, orNote, type ArgsSpec, type VerbSpec, type NounSpec, type HelpTopic, type HelpSpec, type VerbRegistry } from './verb.ts';
@@ -0,0 +1,3 @@
1
+ // Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
2
+ export { field, refusalBody, refusalRows, parseRefusalBody } from './refusal.js';
3
+ export { VerbRefusal, DRY_RUN_FLAG, REVISION_FLAG, DRY_RUN_PREVIEW, summaryLines, createVerbRegistry, lengthOf, requiredField, orNote } from './verb.js';