@adecore/agents 0.23.1 → 0.24.1
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 +3 -3
- package/dist/chat/attachment-store.js +4 -5
- package/dist/chat/backend.d.ts +6 -1
- package/dist/chat/background-work.d.ts +1 -1
- package/dist/chat/background-work.js +2 -1
- package/dist/chat/chat-core.d.ts +53 -161
- package/dist/chat/chat-core.js +146 -112
- package/dist/chat/chat-log.js +3 -3
- package/dist/chat/chat-process.d.ts +21 -0
- package/dist/chat/chat-process.js +109 -9
- package/dist/chat/chat-session.d.ts +10 -17
- package/dist/chat/chat-session.js +141 -182
- package/dist/chat/chat-store.d.ts +1 -0
- package/dist/chat/chat-store.js +105 -1
- package/dist/chat/claude-backend.d.ts +4 -0
- package/dist/chat/claude-backend.js +51 -27
- package/dist/chat/claude-protocol.d.ts +12 -0
- package/dist/chat/claude-protocol.js +137 -7
- package/dist/chat/claude-title.d.ts +1 -3
- package/dist/chat/claude-title.js +11 -39
- package/dist/chat/claude-transcript.d.ts +1 -0
- package/dist/chat/claude-transcript.js +36 -17
- package/dist/chat/codex-backend.d.ts +21 -1
- package/dist/chat/codex-backend.js +168 -10
- package/dist/chat/codex-protocol.d.ts +2 -0
- package/dist/chat/codex-protocol.js +37 -3
- package/dist/chat/codex-thread.js +1 -3
- package/dist/chat/codex-transport.d.ts +3 -0
- package/dist/chat/codex-transport.js +38 -35
- package/dist/chat/context-breakdown.js +2 -2
- package/dist/chat/errors.d.ts +1 -1
- package/dist/chat/external-sessions.js +2 -2
- package/dist/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/dist/chat/index.d.ts +29 -0
- package/dist/chat/index.js +23 -0
- package/dist/chat/projector.d.ts +2 -1
- package/dist/chat/projector.js +12 -3
- package/dist/chat/request-summary.js +1 -4
- package/dist/chat/subagent-settlement.js +1 -3
- package/dist/chat/thread.d.ts +3 -0
- package/dist/chat/thread.js +20 -3
- package/dist/chat/ui-queries.d.ts +2 -0
- package/dist/chat/ui-queries.js +40 -5
- package/dist/chat/ui-replies.d.ts +33 -0
- package/dist/chat/ui-replies.js +152 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +3 -0
- package/dist/coordination/index.d.ts +15 -0
- package/dist/coordination/index.js +16 -0
- package/dist/host/handlers.js +8 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.js +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/claude.js +3 -1
- package/dist/providers/codex.js +3 -1
- package/dist/providers/index.d.ts +14 -0
- package/dist/providers/index.js +12 -0
- package/dist/run-process.js +54 -14
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +4 -0
- package/dist/tasks/task-coordinator.d.ts +4 -0
- package/dist/tasks/task-coordinator.js +16 -2
- package/dist/tasks/wiring.js +7 -1
- package/dist/{providers/accounts/test-accounts.d.ts → testing/accounts.d.ts} +2 -2
- package/dist/{providers/accounts/test-accounts.js → testing/accounts.js} +3 -3
- package/dist/testing/bin/fake-claude.d.ts +1 -0
- package/dist/testing/bin/fake-claude.js +6 -0
- package/dist/testing/bin/fake-codex.d.ts +1 -0
- package/dist/testing/bin/fake-codex.js +6 -0
- package/dist/{chat → testing}/fake-claude.d.ts +1 -1
- package/dist/{chat → testing}/fake-claude.js +0 -4
- package/dist/{chat → testing}/fake-cli.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.d.ts +1 -1
- package/dist/{chat → testing}/fake-codex.js +70 -8
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +7 -0
- package/dist/{outbox → testing}/manual-clock.d.ts +1 -1
- package/dist/{watch-test-helpers.d.ts → testing/watch.d.ts} +1 -1
- package/dist/usage/exchange.d.ts +5 -3
- package/dist/usage/exchange.js +25 -16
- package/dist/usage/index-file.d.ts +1 -0
- package/dist/usage/index-file.js +20 -2
- package/dist/usage/index.d.ts +9 -0
- package/dist/usage/index.js +6 -0
- package/dist/usage/limits/normalize.js +1 -3
- package/dist/usage/pricing.d.ts +5 -3
- package/dist/usage/pricing.js +28 -19
- package/dist/usage/scanner.js +39 -11
- package/dist/usage/usage-service.js +8 -3
- package/package.json +72 -11
- package/src/chat/attachment-store.ts +4 -6
- package/src/chat/backend.ts +8 -0
- package/src/chat/background-work.ts +2 -2
- package/src/chat/chat-core.ts +221 -143
- package/src/chat/chat-log.ts +3 -3
- package/src/chat/chat-process.ts +128 -8
- package/src/chat/chat-session.ts +153 -204
- package/src/chat/chat-store.ts +104 -1
- package/src/chat/claude-backend.ts +58 -26
- package/src/chat/claude-protocol.ts +147 -8
- package/src/chat/claude-title.ts +11 -39
- package/src/chat/claude-transcript.ts +38 -18
- package/src/chat/codex-backend.ts +182 -20
- package/src/chat/codex-protocol.ts +40 -5
- package/src/chat/codex-thread.ts +1 -4
- package/src/chat/codex-transport.ts +45 -35
- package/src/chat/context-breakdown.ts +2 -2
- package/src/chat/errors.ts +1 -0
- package/src/chat/external-sessions.ts +2 -2
- package/src/chat/fixtures/claude-2.1.296-goal.json +136 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl +2 -0
- package/src/chat/index.ts +49 -0
- package/src/chat/projector.ts +13 -4
- package/src/chat/request-summary.ts +8 -5
- package/src/chat/subagent-settlement.ts +1 -4
- package/src/chat/thread.ts +22 -3
- package/src/chat/ui-queries.ts +46 -5
- package/src/chat/ui-replies.ts +187 -0
- package/src/context/index.ts +26 -0
- package/src/coordination/index.ts +67 -0
- package/src/host/handlers.ts +8 -0
- package/src/host/index.ts +8 -0
- package/src/index.ts +20 -0
- package/src/providers/accounts/status.ts +1 -4
- package/src/providers/claude.ts +3 -1
- package/src/providers/codex.ts +3 -1
- package/src/providers/index.ts +15 -0
- package/src/run-process.ts +56 -16
- package/src/skills.ts +5 -0
- package/src/tasks/task-coordinator.ts +22 -2
- package/src/tasks/wiring.ts +7 -1
- package/src/{providers/accounts/test-accounts.ts → testing/accounts.ts} +4 -4
- package/src/testing/bin/fake-claude.ts +7 -0
- package/src/testing/bin/fake-codex.ts +7 -0
- package/src/{chat → testing}/fake-claude.ts +1 -5
- package/src/{chat → testing}/fake-cli.ts +1 -1
- package/src/{chat → testing}/fake-codex.ts +75 -6
- package/src/testing/index.ts +7 -0
- package/src/{outbox → testing}/manual-clock.ts +1 -1
- package/src/{watch-test-helpers.ts → testing/watch.ts} +1 -1
- package/src/usage/exchange.ts +31 -20
- package/src/usage/index-file.ts +23 -2
- package/src/usage/index.ts +10 -0
- package/src/usage/limits/normalize.ts +1 -4
- package/src/usage/pricing.ts +34 -23
- package/src/usage/scanner.ts +48 -11
- package/src/usage/usage-service.ts +8 -3
- /package/dist/{chat → testing}/fake-cli.js +0 -0
- /package/dist/{outbox → testing}/manual-clock.js +0 -0
- /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
|
+
}
|
package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10.jsonl
ADDED
|
@@ -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,49 @@
|
|
|
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.ts';
|
|
3
|
+
export type { BackendLaunch, TurnInput, ApprovalDecision, BackendEvent, BackendHost, ChatBackend } from './backend.ts';
|
|
4
|
+
export { runningInBackground, type BackgroundWork } from './background-work.ts';
|
|
5
|
+
export { BookmarkStore, type BookmarkListener } from './bookmark-store.ts';
|
|
6
|
+
export { type InterruptedRun, type ChatCoreHooks, type ChatCoreOptions, ChatCore } from './chat-core.ts';
|
|
7
|
+
export { COMPACT_ABOVE_BYTES, type ChatLogLine } from './chat-log.ts';
|
|
8
|
+
export {
|
|
9
|
+
type ChatSpawnOptions,
|
|
10
|
+
type SpawnChatProcess,
|
|
11
|
+
spawnChatProcess,
|
|
12
|
+
MAX_LINE_LENGTH,
|
|
13
|
+
LineSplitter,
|
|
14
|
+
ChatChild,
|
|
15
|
+
type ChatProcess,
|
|
16
|
+
type ChatChildOptions
|
|
17
|
+
} from './chat-process.ts';
|
|
18
|
+
export {
|
|
19
|
+
type PromptNotes,
|
|
20
|
+
type ChatReferences,
|
|
21
|
+
type ResumeWords,
|
|
22
|
+
ChatSession,
|
|
23
|
+
type TurnCheckpoints,
|
|
24
|
+
type ChatSessionOptions,
|
|
25
|
+
type LimitResumeHooks,
|
|
26
|
+
type ChatSendExtras,
|
|
27
|
+
type ResumeDecision
|
|
28
|
+
} from './chat-session.ts';
|
|
29
|
+
export { type ChatRecordExtras, type ChatRecord, ChatStore, type ChatSeq, type ChatStoreOptions } from './chat-store.ts';
|
|
30
|
+
export { suggestChatTitle, type ChatTitleInput } from './chat-title.ts';
|
|
31
|
+
export type { ClaudeBackendOptions } from './claude-backend.ts';
|
|
32
|
+
export { ClaudeTitleReader } from './claude-title.ts';
|
|
33
|
+
export { spokenText, claudeProjectSlug, findClaudeSession } from './claude-transcript.ts';
|
|
34
|
+
export type { CodexBackendOptions } from './codex-backend.ts';
|
|
35
|
+
export { forkThreadOnce, type ThreadItemsParams, type CodexProcessSpec, type ThreadForkParams } from './codex-thread.ts';
|
|
36
|
+
export type { CodexClientInfo } from './codex-transport.ts';
|
|
37
|
+
export type { ComposerPreference } from './composer-preferences.ts';
|
|
38
|
+
export { type ChatErrorCode, ChatError } from './errors.ts';
|
|
39
|
+
export { type SessionFs, type ExternalSession, listClaudeSessions, listCodexSessions, nodeSessionFs } from './external-sessions.ts';
|
|
40
|
+
export { limitedTurn, limitResumeAt } from './limit-resume.ts';
|
|
41
|
+
export { readableAssistantText } from './readable-text.ts';
|
|
42
|
+
export type { SubagentChat, SubagentReaderOptions } from './subagent-reader.ts';
|
|
43
|
+
export type { SubagentSettlement } from './subagent-settlement.ts';
|
|
44
|
+
export { searchThreadItems, type ThreadHit, type ThreadSearchOptions } from './thread-search.ts';
|
|
45
|
+
export { ChatThread } from './thread.ts';
|
|
46
|
+
export { type ChatUiSource, type ChatUiHost, ChatUiRefusal } from './ui-queries.ts';
|
|
47
|
+
export { visualFileName, VisualStore, type VisualInput, type VisualStoreOptions, type VisualListener } from './visual-store.ts';
|
|
48
|
+
export { type WakeChat, chatOpener, type ChatOpenerDeps } from './wake-chat.ts';
|
|
49
|
+
export { type SkillRootOptions, SkillIndex } from '../skills.ts';
|
package/src/chat/projector.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ChatToolProgress,
|
|
9
9
|
ChatTurnLimit
|
|
10
10
|
} from '@adecore/agent-contracts';
|
|
11
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
11
12
|
import { clipText } from '@adecore/agent-contracts/text';
|
|
12
13
|
import type { BackendEvent } from './backend.ts';
|
|
13
14
|
import { estimateContextBreakdown } from './context-breakdown.ts';
|
|
@@ -40,10 +41,6 @@ function startsAgentTurn(event: BackendEvent): boolean {
|
|
|
40
41
|
return isMainAgentOutput(event) || ((event.type === 'approval.requested' || event.type === 'question.requested') && event.background !== true);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
44
|
function str(value: unknown): string | null {
|
|
48
45
|
return typeof value === 'string' ? value : null;
|
|
49
46
|
}
|
|
@@ -381,6 +378,13 @@ export class ThreadProjector {
|
|
|
381
378
|
case 'model':
|
|
382
379
|
events.push(this.thread.patchInfo({ model: event.model }));
|
|
383
380
|
break;
|
|
381
|
+
case 'goal': {
|
|
382
|
+
const goal = event.goal ?? undefined;
|
|
383
|
+
if (JSON.stringify(goal) !== JSON.stringify(info.goal)) {
|
|
384
|
+
events.push(this.thread.patchInfo({ goal }));
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
384
388
|
case 'note':
|
|
385
389
|
events.push(this.note(event.level, event.text));
|
|
386
390
|
break;
|
|
@@ -467,6 +471,11 @@ export class ThreadProjector {
|
|
|
467
471
|
return event.type === 'tool.started' && this.thread.get(this.itemId(generation, event.ref)) !== undefined;
|
|
468
472
|
}
|
|
469
473
|
|
|
474
|
+
/* The item a ref of that generation names, without a walk through the thread. */
|
|
475
|
+
itemOf(generation: number, ref: string): ChatItem | undefined {
|
|
476
|
+
return this.thread.get(this.itemId(generation, ref));
|
|
477
|
+
}
|
|
478
|
+
|
|
470
479
|
private itemId(generation: number, ref: string): string {
|
|
471
480
|
return `${generation}:${ref}`;
|
|
472
481
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
isRecord,
|
|
3
|
+
CHAT_REQUEST_LIMITS,
|
|
4
|
+
type ChatApprovalItem,
|
|
5
|
+
type ChatQuestionItem,
|
|
6
|
+
type ChatRequestApproval,
|
|
7
|
+
type ChatRequestSummary
|
|
8
|
+
} from '@adecore/agent-contracts';
|
|
6
9
|
|
|
7
10
|
function str(value: unknown): string | null {
|
|
8
11
|
return typeof value === 'string' ? value : null;
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { open, stat } from 'node:fs/promises';
|
|
2
|
+
import { isRecord } from '@adecore/agent-contracts';
|
|
2
3
|
|
|
3
4
|
// The end of a transcript is all the question needs; a final line longer than this reads as undecided.
|
|
4
5
|
const TAIL_BYTES = 1024 * 1024;
|
|
5
6
|
|
|
6
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
7
|
/* How a subagent's own transcript says it ended: when, and the text of its last message. */
|
|
11
8
|
export interface SubagentSettlement {
|
|
12
9
|
finishedAt: number | null;
|
package/src/chat/thread.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
3
|
+
import { appendToolOutput } from '@adecore/agent-contracts/text';
|
|
3
4
|
import { ChatError } from './errors.ts';
|
|
4
5
|
import { requestSummaries } from './request-summary.ts';
|
|
5
6
|
|
|
@@ -13,6 +14,8 @@ export class ChatThread {
|
|
|
13
14
|
private readonly order: string[] = [];
|
|
14
15
|
private historyGeneration = randomUUID();
|
|
15
16
|
private readonly indices = new Map<string, number>();
|
|
17
|
+
// What the host asks a person beside the CLI's own requests, such as a card a verb waits on.
|
|
18
|
+
private readonly holds = new Set<string>();
|
|
16
19
|
|
|
17
20
|
constructor(info: ChatInfo, items: ChatItem[] = []) {
|
|
18
21
|
this.info = info;
|
|
@@ -63,17 +66,33 @@ export class ChatThread {
|
|
|
63
66
|
}
|
|
64
67
|
if (item?.kind === 'tool' && item.state === 'running') {
|
|
65
68
|
const progress = item.progress ?? { startedAt: null, description: null, output: null };
|
|
66
|
-
this.items.set(itemId, { ...item, progress: { ...progress, output: (progress.output
|
|
69
|
+
this.items.set(itemId, { ...item, progress: { ...progress, output: appendToolOutput(progress.output, text) } });
|
|
67
70
|
return { type: 'delta', itemId, text };
|
|
68
71
|
}
|
|
69
72
|
return null;
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
/* While the host holds the chat for a person, a turn that runs reads as waiting on them. */
|
|
72
76
|
patchInfo(patch: Partial<ChatInfo>): ChatEvent {
|
|
73
|
-
|
|
77
|
+
const held = patch.status === 'running' && this.holds.size > 0 ? { ...patch, status: 'needs-you' as const } : patch;
|
|
78
|
+
this.info = { ...this.info, ...held };
|
|
74
79
|
return { type: 'info', info: this.info };
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/* False when the key held the chat already. */
|
|
83
|
+
hold(key: string): boolean {
|
|
84
|
+
if (this.holds.has(key)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
this.holds.add(key);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* False when the key did not hold the chat. */
|
|
92
|
+
release(key: string): boolean {
|
|
93
|
+
return this.holds.delete(key);
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
/* Empties the thread; the info carries on with the patch on top. */
|
|
78
97
|
reset(patch: Partial<ChatInfo>): ChatEvent {
|
|
79
98
|
this.items.clear();
|
|
@@ -118,7 +137,7 @@ export class ChatThread {
|
|
|
118
137
|
|
|
119
138
|
/* A request can outlive the turn it came in, when work the CLI runs beside its turns asked it. */
|
|
120
139
|
statusFor(activeTurnId: string | null): AgentStatus {
|
|
121
|
-
if (this.pending().length > 0) {
|
|
140
|
+
if (this.pending().length > 0 || this.holds.size > 0) {
|
|
122
141
|
return 'needs-you';
|
|
123
142
|
}
|
|
124
143
|
return activeTurnId === null ? 'idle' : 'running';
|
package/src/chat/ui-queries.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface ChatUiHost {
|
|
|
31
31
|
|
|
32
32
|
interface CachedReading {
|
|
33
33
|
key: string;
|
|
34
|
+
// The key without the arguments: one query of one block, whatever it was read with.
|
|
35
|
+
identity: string;
|
|
34
36
|
chatId: string;
|
|
35
37
|
reading: ChatUiQueryReading;
|
|
36
38
|
}
|
|
@@ -71,6 +73,8 @@ export class ChatUiQueries {
|
|
|
71
73
|
private readonly cache = new Map<string, CachedReading>();
|
|
72
74
|
// The read id of the newest reading per cache key, which a remount or a second client is answered with.
|
|
73
75
|
private readonly latest = new Map<string, string>();
|
|
76
|
+
// The read id of the newest reading per query identity, whose options are the ones a person picks from.
|
|
77
|
+
private readonly newest = new Map<string, string>();
|
|
74
78
|
private readonly reading = new Map<string, Promise<ChatUiQueryReading>>();
|
|
75
79
|
private readonly attempts = new Map<string, number>();
|
|
76
80
|
private readonly active = new Map<string, number>();
|
|
@@ -237,7 +241,7 @@ export class ChatUiQueries {
|
|
|
237
241
|
let authorAccess: unknown;
|
|
238
242
|
try {
|
|
239
243
|
({ source, block, item } = this.stored(session, payload));
|
|
240
|
-
args = uiQueryArguments(block, payload.query, payload.values);
|
|
244
|
+
args = uiQueryArguments(block, payload.query, payload.values, {}, this.heldValues(session, item, block));
|
|
241
245
|
args = source.args.parse(args) as Record<string, UiValue>;
|
|
242
246
|
authorAccess = await this.access(session, item);
|
|
243
247
|
if (authorAccess === null) {
|
|
@@ -292,8 +296,9 @@ export class ChatUiQueries {
|
|
|
292
296
|
throw new UiQueryError('refused-query', 'result-too-large', 'The UI query result is too large.');
|
|
293
297
|
}
|
|
294
298
|
const reading: ChatUiQueryReading = { state: 'fresh', value, readId: randomUUID(), readAt: Date.now() };
|
|
295
|
-
this.cache.set(reading.readId!, { key, chatId: session.id, reading });
|
|
299
|
+
this.cache.set(reading.readId!, { key, identity, chatId: session.id, reading });
|
|
296
300
|
this.latest.set(key, reading.readId!);
|
|
301
|
+
this.newest.set(identity, reading.readId!);
|
|
297
302
|
while (this.cache.size > 128) {
|
|
298
303
|
this.evict(this.cache.keys().next().value!);
|
|
299
304
|
}
|
|
@@ -336,14 +341,31 @@ export class ChatUiQueries {
|
|
|
336
341
|
if (access === null) {
|
|
337
342
|
throw new UiQueryError('refused-query', 'access-unavailable', 'The writer’s original access is unavailable.');
|
|
338
343
|
}
|
|
344
|
+
const frozen = item.uiQueries?.blocks[block.id];
|
|
345
|
+
const readIdOf = (name: string): string | undefined =>
|
|
346
|
+
payload.reads !== undefined && Object.hasOwn(payload.reads, name) ? payload.reads[name] : undefined;
|
|
347
|
+
// What the person saw, by the read ids it names; each one is checked against its arguments below.
|
|
348
|
+
const seen: Record<string, unknown> = Object.create(null);
|
|
349
|
+
for (const name of names) {
|
|
350
|
+
const readId = readIdOf(name);
|
|
351
|
+
const cached = readId === undefined ? undefined : this.cache.get(readId);
|
|
352
|
+
const reading =
|
|
353
|
+
cached?.identity === JSON.stringify([session.id, item.id, block.id, block.revision, name])
|
|
354
|
+
? cached.reading
|
|
355
|
+
: frozen?.revision === block.revision
|
|
356
|
+
? frozen?.readings[name]
|
|
357
|
+
: undefined;
|
|
358
|
+
if (readId !== undefined && reading?.readId === readId && reading.state === 'fresh') {
|
|
359
|
+
seen[name] = reading.value;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
339
362
|
for (const name of names) {
|
|
340
363
|
const { source } = this.stored(session, { ...payload, query: name });
|
|
341
|
-
const args = source.args.parse(uiQueryArguments(block, name, payload.values)) as Record<string, UiValue>;
|
|
364
|
+
const args = source.args.parse(uiQueryArguments(block, name, payload.values, {}, seen)) as Record<string, UiValue>;
|
|
342
365
|
await source.authorize(session.info, access, args);
|
|
343
|
-
const readId =
|
|
366
|
+
const readId = readIdOf(name);
|
|
344
367
|
const key = JSON.stringify([session.id, item.id, block.id, block.revision, name, args]);
|
|
345
368
|
const cached = readId === undefined ? undefined : this.cache.get(readId);
|
|
346
|
-
const frozen = item.uiQueries?.blocks[block.id];
|
|
347
369
|
const snapshot = frozen?.revision === block.revision ? frozen?.readings[name] : undefined;
|
|
348
370
|
const reading =
|
|
349
371
|
readId === undefined
|
|
@@ -399,6 +421,25 @@ export class ChatUiQueries {
|
|
|
399
421
|
if (cached && this.latest.get(cached.key) === readId) {
|
|
400
422
|
this.latest.delete(cached.key);
|
|
401
423
|
}
|
|
424
|
+
if (cached && this.newest.get(cached.identity) === readId) {
|
|
425
|
+
this.newest.delete(cached.identity);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* The newest fresh value of every query of a block, or the frozen one, for checking an input chosen from what they read. */
|
|
430
|
+
private heldValues(session: ChatSession, item: ChatAssistantItem, block: UiBlock): Record<string, unknown> {
|
|
431
|
+
const frozen = item.uiQueries?.blocks[block.id];
|
|
432
|
+
const values: Record<string, unknown> = Object.create(null);
|
|
433
|
+
for (const name of Object.keys(block.queries)) {
|
|
434
|
+
const readId = this.newest.get(JSON.stringify([session.id, item.id, block.id, block.revision, name]));
|
|
435
|
+
const reading =
|
|
436
|
+
(readId === undefined ? undefined : this.cache.get(readId)?.reading) ??
|
|
437
|
+
(frozen?.revision === block.revision ? frozen?.readings[name] : undefined);
|
|
438
|
+
if (reading?.state === 'fresh') {
|
|
439
|
+
values[name] = reading.value;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return values;
|
|
402
443
|
}
|
|
403
444
|
|
|
404
445
|
private linkReading(value: unknown): ChatUiLinkReading {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { GeneratedImageInputSchema, type ChatAssistantItem, type ChatEvent, type ChatItem } from '@adecore/agent-contracts';
|
|
3
|
+
import { uiHasFence, uiMayReferenceHost } from '@adecore/intelligent-ui';
|
|
4
|
+
import type { UiCompileOptions } from '@adecore/intelligent-ui/compiler';
|
|
5
|
+
import { UiStream, type UiStreamPreview } from '@adecore/intelligent-ui/stream';
|
|
6
|
+
import type { OutboxClock } from '../outbox/outbox-worker.ts';
|
|
7
|
+
import type { ChatThread } from './thread.ts';
|
|
8
|
+
|
|
9
|
+
// Longer than any fence line or the name of a tag a scan looks for, so a match never falls between two deltas.
|
|
10
|
+
const UI_SCAN_OVERLAP = 64;
|
|
11
|
+
|
|
12
|
+
/* What a host answers about the UI blocks of a reply: the schemas of its query sources, and a look at each reply that may read them. */
|
|
13
|
+
export interface UiReplyHost {
|
|
14
|
+
schemas: UiCompileOptions['querySchemas'];
|
|
15
|
+
observe(item: ChatAssistantItem, final: boolean): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UiRepliesOptions {
|
|
19
|
+
thread: ChatThread;
|
|
20
|
+
fenceLanguage: string;
|
|
21
|
+
clock: OutboxClock;
|
|
22
|
+
host?: UiReplyHost;
|
|
23
|
+
emit(event: ChatEvent): void;
|
|
24
|
+
// What the agent hears in front of its next prompt about a block that compiled with diagnostics.
|
|
25
|
+
repair(note: string): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* The UI blocks in the replies of a chat's main agent: every event of the chat passes through here on
|
|
30
|
+
* its way out, and a reply with a UI fence in it is compiled as it streams, with a preview per delta
|
|
31
|
+
* and the finished blocks on the item once it settles.
|
|
32
|
+
*/
|
|
33
|
+
export class UiReplies {
|
|
34
|
+
private readonly options: UiRepliesOptions;
|
|
35
|
+
private readonly streams = new Map<string, UiStream>();
|
|
36
|
+
private readonly compiled = new Map<string, UiStreamPreview>();
|
|
37
|
+
// How much of a streaming reply's text the fence and host scans already covered, so a delta scans only its tail.
|
|
38
|
+
private readonly scanned = new Map<string, number>();
|
|
39
|
+
private readonly referencing = new Set<string>();
|
|
40
|
+
private readonly attachments = new Map<string | null, string | undefined>();
|
|
41
|
+
|
|
42
|
+
constructor(options: UiRepliesOptions) {
|
|
43
|
+
this.options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* The preview of every reply still streaming, as deltas, for a client that attached in the middle of one. */
|
|
47
|
+
previews(): ChatEvent[] {
|
|
48
|
+
const events: ChatEvent[] = [];
|
|
49
|
+
for (const [itemId, preview] of this.compiled) {
|
|
50
|
+
const item = this.options.thread.get(itemId);
|
|
51
|
+
if (item?.kind === 'assistant' && item.streaming && item.text.length === preview.textLength) {
|
|
52
|
+
events.push({ type: 'delta', itemId, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return events;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
previewItems(items: ChatItem[]): ChatItem[] {
|
|
59
|
+
return items.map((item) => {
|
|
60
|
+
const preview = this.compiled.get(item.id);
|
|
61
|
+
return item.kind === 'assistant' && preview?.textLength === item.text.length ? { ...item, ui: preview.blocks } : item;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Hands one event of the chat on, with the blocks of a reply it finished in place of its fences. */
|
|
66
|
+
pass(event: ChatEvent): void {
|
|
67
|
+
const { thread, emit } = this.options;
|
|
68
|
+
if (event.type === 'reset') {
|
|
69
|
+
this.clear();
|
|
70
|
+
}
|
|
71
|
+
if (event.type === 'item' && event.item.kind === 'tool') {
|
|
72
|
+
this.attachments.delete(event.item.turnId);
|
|
73
|
+
}
|
|
74
|
+
const item = event.type === 'item' ? event.item : event.type === 'delta' ? thread.get(event.itemId) : null;
|
|
75
|
+
if (item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
76
|
+
emit(event);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const tail = this.unscannedText(item, event.type === 'item');
|
|
80
|
+
if (uiMayReferenceHost(tail)) {
|
|
81
|
+
this.referencing.add(item.id);
|
|
82
|
+
}
|
|
83
|
+
let stream = this.streams.get(item.id);
|
|
84
|
+
if (!stream && !uiHasFence(tail, this.options.fenceLanguage)) {
|
|
85
|
+
this.forgetScan(item);
|
|
86
|
+
emit(event);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!stream) {
|
|
90
|
+
stream = new UiStream({
|
|
91
|
+
querySchemas: this.options.host?.schemas,
|
|
92
|
+
fenceLanguage: this.options.fenceLanguage,
|
|
93
|
+
id: item.id,
|
|
94
|
+
clock: this.options.clock,
|
|
95
|
+
emit: (preview) => {
|
|
96
|
+
this.compiled.set(item.id, preview);
|
|
97
|
+
emit({ type: 'delta', itemId: item.id, text: '', ui: preview.blocks, textLength: preview.textLength });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.streams.set(item.id, stream);
|
|
101
|
+
}
|
|
102
|
+
if (this.referencing.has(item.id)) {
|
|
103
|
+
this.options.host?.observe(item, false);
|
|
104
|
+
}
|
|
105
|
+
this.forgetScan(item);
|
|
106
|
+
const latestAttachment = this.latestAttachment(item.turnId);
|
|
107
|
+
if (event.type !== 'item' || item.streaming) {
|
|
108
|
+
emit(event);
|
|
109
|
+
stream.update(item.text, latestAttachment);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const ui = stream
|
|
113
|
+
.finish(item.text, latestAttachment)
|
|
114
|
+
.map((block) => ({ ...block, revision: createHash('sha256').update(JSON.stringify(block)).digest('hex') }));
|
|
115
|
+
this.streams.delete(item.id);
|
|
116
|
+
this.compiled.delete(item.id);
|
|
117
|
+
const diagnostics = [...new Set(ui.flatMap((block) => block.diagnostics.map((diagnostic) => diagnostic.message)))].slice(0, 6);
|
|
118
|
+
if (diagnostics.length > 0) {
|
|
119
|
+
this.options.repair(
|
|
120
|
+
`Your last ${this.options.fenceLanguage} block had these diagnostics: ${diagnostics.join(' ')} Use the catalog's supported props and components.`.slice(
|
|
121
|
+
0,
|
|
122
|
+
1200
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const current = thread.get(item.id);
|
|
127
|
+
const completed = { ...item, ui, ...(current?.kind === 'assistant' && current.uiQueries ? { uiQueries: current.uiQueries } : {}) };
|
|
128
|
+
emit(thread.upsert(completed));
|
|
129
|
+
this.options.host?.observe(completed, true);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
clear(): void {
|
|
133
|
+
for (const stream of this.streams.values()) {
|
|
134
|
+
stream.dispose();
|
|
135
|
+
}
|
|
136
|
+
this.streams.clear();
|
|
137
|
+
this.compiled.clear();
|
|
138
|
+
this.scanned.clear();
|
|
139
|
+
this.referencing.clear();
|
|
140
|
+
this.attachments.clear();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* The text a scan has not covered yet, from the start of the line the last scan ended in, since a fence
|
|
145
|
+
* is one line; a long line is cut short, behind a character no fence or tag can follow.
|
|
146
|
+
*/
|
|
147
|
+
private unscannedText(item: ChatAssistantItem, whole: boolean): string {
|
|
148
|
+
const scanned = whole ? 0 : (this.scanned.get(item.id) ?? 0);
|
|
149
|
+
this.scanned.set(item.id, item.text.length);
|
|
150
|
+
if (scanned === 0 || scanned > item.text.length) {
|
|
151
|
+
return item.text;
|
|
152
|
+
}
|
|
153
|
+
const line = item.text.lastIndexOf('\n', scanned - 1) + 1;
|
|
154
|
+
const from = Math.max(line, scanned - UI_SCAN_OVERLAP);
|
|
155
|
+
return from === line ? item.text.slice(from) : `\0${item.text.slice(from)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private forgetScan(item: ChatAssistantItem): void {
|
|
159
|
+
if (!item.streaming) {
|
|
160
|
+
this.scanned.delete(item.id);
|
|
161
|
+
this.referencing.delete(item.id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Looked up once per turn, and again only after one of its tool calls changed. */
|
|
166
|
+
private latestAttachment(turnId: string | null): string | undefined {
|
|
167
|
+
if (this.attachments.has(turnId)) {
|
|
168
|
+
return this.attachments.get(turnId);
|
|
169
|
+
}
|
|
170
|
+
const attachment = this.findAttachment(turnId);
|
|
171
|
+
this.attachments.set(turnId, attachment);
|
|
172
|
+
return attachment;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private findAttachment(turnId: string | null): string | undefined {
|
|
176
|
+
for (const item of this.options.thread.list().toReversed()) {
|
|
177
|
+
if (item.kind !== 'tool' || item.turnId !== turnId || item.name !== 'ImageGeneration' || item.state !== 'done') {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const input = GeneratedImageInputSchema.safeParse(item.input);
|
|
181
|
+
if (input.success && input.data.attachment?.mime.startsWith('image/')) {
|
|
182
|
+
return input.data.attachment.id;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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, type ParsedRefusal, parseRefusalBody } from './refusal.ts';
|
|
3
|
+
export {
|
|
4
|
+
VerbRefusal,
|
|
5
|
+
type VerbCallBase,
|
|
6
|
+
type VerbHelp,
|
|
7
|
+
type Verb,
|
|
8
|
+
type Action,
|
|
9
|
+
type Noun,
|
|
10
|
+
type ContextVerb,
|
|
11
|
+
type VerbEntry,
|
|
12
|
+
DRY_RUN_FLAG,
|
|
13
|
+
REVISION_FLAG,
|
|
14
|
+
DRY_RUN_PREVIEW,
|
|
15
|
+
summaryLines,
|
|
16
|
+
createVerbRegistry,
|
|
17
|
+
lengthOf,
|
|
18
|
+
requiredField,
|
|
19
|
+
orNote,
|
|
20
|
+
type ArgsSpec,
|
|
21
|
+
type VerbSpec,
|
|
22
|
+
type NounSpec,
|
|
23
|
+
type HelpTopic,
|
|
24
|
+
type HelpSpec,
|
|
25
|
+
type VerbRegistry
|
|
26
|
+
} from './verb.ts';
|