@adecore/agent-contracts 0.0.1 → 0.17.0-beta.2

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/dist/chat.js ADDED
@@ -0,0 +1,741 @@
1
+ import { z } from 'zod';
2
+ import { AgentKindSchema, AgentStatusSchema, SuggestedTitleSchema } from './agent.js';
3
+ import { WorktreeSchema } from './worktree.js';
4
+ import { ModelSelectionSchema, RuntimeModeSchema } from './model.js';
5
+ import { ProviderAccountIdSchema } from './provider-accounts.js';
6
+ // The client picks the id (its node id), like a terminal session.
7
+ export const ChatIdSchema = z.string().min(1);
8
+ // The daemon's estimate of what `contextTokens` is made of, scaled to add up to it; `system` is whatever the thread cannot account for.
9
+ export const ChatContextBreakdownSchema = z.object({
10
+ toolOutput: z.number().int().nonnegative(),
11
+ filesRead: z.number().int().nonnegative(),
12
+ conversation: z.number().int().nonnegative(),
13
+ system: z.number().int().nonnegative()
14
+ });
15
+ export const ChatUsageSchema = z.object({
16
+ // Tokens the last request carried, which is what the model saw as its context.
17
+ contextTokens: z.number().int().nonnegative(),
18
+ contextWindow: z.number().int().positive().nullable(),
19
+ costUsd: z.number().nonnegative(),
20
+ turns: z.number().int().nonnegative(),
21
+ // Absent while nothing is in the context yet, and from a daemon that does not estimate.
22
+ breakdown: ChatContextBreakdownSchema.optional()
23
+ });
24
+ // Base64 for 10 MiB leaves room for the prompt and envelope inside the 16 MiB transport frame.
25
+ export const CHAT_ATTACHMENTS_MAX_BYTES = 10 * 1024 * 1024;
26
+ export const CHAT_ATTACHMENT_MAX_BYTES = CHAT_ATTACHMENTS_MAX_BYTES;
27
+ export const CHAT_ATTACHMENTS_MAX_COUNT = 8;
28
+ const MAX_BASE64_LENGTH = Math.ceil(CHAT_ATTACHMENT_MAX_BYTES / 3) * 4;
29
+ export function attachmentBytes(data) {
30
+ return Math.floor((data.length * 3) / 4) - (data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0);
31
+ }
32
+ function isBase64(data) {
33
+ const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;
34
+ return data.length % 4 === 0 && !/[^A-Za-z0-9+/]/.test(data.slice(0, data.length - padding));
35
+ }
36
+ const IMAGE_MIME_BY_EXTENSION = new Map([
37
+ ['png', 'image/png'],
38
+ ['jpg', 'image/jpeg'],
39
+ ['jpeg', 'image/jpeg'],
40
+ ['webp', 'image/webp'],
41
+ ['gif', 'image/gif']
42
+ ]);
43
+ const IMAGE_MIME_TYPES = new Set(IMAGE_MIME_BY_EXTENSION.values());
44
+ // A generic MIME type from Finder may still name a picture; a declared PDF must stay a PDF.
45
+ export function attachmentImageMime({ name, mime }) {
46
+ const type = mime.split(';')[0].trim().toLowerCase();
47
+ if (IMAGE_MIME_TYPES.has(type)) {
48
+ return type;
49
+ }
50
+ if (type !== '' && type !== 'application/octet-stream' && type !== 'binary/octet-stream') {
51
+ return null;
52
+ }
53
+ const dot = name.lastIndexOf('.');
54
+ return dot < 0 ? null : (IMAGE_MIME_BY_EXTENSION.get(name.slice(dot + 1).toLowerCase()) ?? null);
55
+ }
56
+ // What the composer hands the daemon: the bytes, plus what the file is called and what it is.
57
+ export const ChatAttachmentUploadSchema = z.object({
58
+ name: z.string().min(1).max(255),
59
+ mime: z.string().min(1).max(255),
60
+ // Base64 without a data-URL prefix.
61
+ data: z
62
+ .string()
63
+ .min(1)
64
+ .max(MAX_BASE64_LENGTH)
65
+ .refine(isBase64, { message: 'Invalid attachment base64' })
66
+ .refine((data) => attachmentBytes(data) <= CHAT_ATTACHMENT_MAX_BYTES, { message: 'Attachment exceeds 10 MiB' })
67
+ });
68
+ export const ChatAttachmentUploadsSchema = z
69
+ .array(ChatAttachmentUploadSchema)
70
+ .max(CHAT_ATTACHMENTS_MAX_COUNT)
71
+ .refine((uploads) => uploads.reduce((bytes, upload) => bytes + attachmentBytes(upload.data), 0) <= CHAT_ATTACHMENTS_MAX_BYTES, {
72
+ message: 'Attachments must total at most 10 MiB per message'
73
+ });
74
+ // What the thread keeps. The bytes live in the host attachment store, so a thread with
75
+ // a video in it is still a small JSON file.
76
+ export const ChatAttachmentSchema = z.object({
77
+ id: z.string().min(1),
78
+ name: z.string().min(1),
79
+ mime: z.string(),
80
+ size: z.number().int().nonnegative(),
81
+ // Absolute, on the machine the daemon runs on.
82
+ path: z.string()
83
+ });
84
+ // A message typed while a turn was running; the daemon sends it when that turn settles.
85
+ export const ChatQueuedMessageSchema = z.object({
86
+ id: z.string().min(1),
87
+ // Older saved queues have no reserved turn yet; draining them still mints one.
88
+ turnId: z.string().min(1).optional(),
89
+ text: z.string(),
90
+ mentions: z.array(z.string()).optional(),
91
+ skills: z.array(z.string()).optional(),
92
+ chats: z.array(z.string()).optional(),
93
+ attachments: z.array(ChatAttachmentSchema).optional(),
94
+ createdAt: z.number()
95
+ });
96
+ // A command or a monitor the CLI keeps running beside its turns, until it ends or its process does.
97
+ export const ChatBackgroundTaskSchema = z.object({
98
+ // The CLI's own id for the task, which is what stopping it names.
99
+ id: z.string().min(1),
100
+ kind: z.enum(['shell', 'monitor']),
101
+ description: z.string(),
102
+ command: z.string().nullable(),
103
+ startedAt: z.number()
104
+ });
105
+ /*
106
+ * Why a turn stopped short, when its CLI said so in its own stream: the plan's usage limit, or a model
107
+ * too busy to answer. Not a state of its own, since a client that validates `state` knows four.
108
+ */
109
+ export const ChatTurnLimitSchema = z.object({
110
+ kind: z.enum(['usage', 'overload']),
111
+ // When the limit lifts, in milliseconds since the epoch; absent when the CLI named no time.
112
+ resetsAt: z.number().optional()
113
+ });
114
+ export const ChatQuestionSchema = z.object({
115
+ id: z.string(),
116
+ header: z.string(),
117
+ question: z.string(),
118
+ choices: z.array(z.object({ label: z.string(), description: z.string() })),
119
+ multiSelect: z.boolean()
120
+ });
121
+ export const ChatRequestKindSchema = z.enum(['approval', 'question']);
122
+ /*
123
+ * Where a request summary stops, in UTF-16 code units and lines. The schema holds no `max` on purpose: one
124
+ * cut a daemon got wrong would refuse every `chat.list` that carried it. Choice labels and question ids stay
125
+ * whole, since an answer is the label itself.
126
+ */
127
+ export const CHAT_REQUEST_LIMITS = {
128
+ perChat: 8,
129
+ subject: 200,
130
+ description: 300,
131
+ command: 1000,
132
+ commandLines: 12,
133
+ diff: 1500,
134
+ diffLines: 12,
135
+ question: 500,
136
+ header: 100,
137
+ choiceDescription: 200
138
+ };
139
+ export const ChatRequestApprovalSchema = z.object({
140
+ toolName: z.string(),
141
+ // One line on what the call is about: the file, the command, the address or the pattern; empty when the input names none.
142
+ subject: z.string(),
143
+ description: z.string().nullable(),
144
+ // The file the diff is of, and how many files the change touches when that is more than one.
145
+ path: z.string().optional(),
146
+ files: z.number().int().optional(),
147
+ // The lines the change takes out and puts in, each with its `-` or `+`, without the context around them.
148
+ diff: z.string().optional(),
149
+ command: z.string().optional(),
150
+ // Set when the diff or the command was cut short; the whole call is in the thread.
151
+ truncated: z.boolean().optional(),
152
+ canAllowAlways: z.boolean(),
153
+ allowAlways: z.object({ label: z.string(), description: z.string() }).optional()
154
+ });
155
+ /*
156
+ * An approval or a question a chat waits on, cut down to what a card needs to show and answer it with
157
+ * `chat.approve`, `chat.answer` or `chat.dismiss`, so a client need not attach the chat to read it.
158
+ */
159
+ export const ChatRequestSummarySchema = z.object({
160
+ requestId: z.string(),
161
+ // The thread item, which `chat.dismiss` takes.
162
+ itemId: z.string(),
163
+ kind: ChatRequestKindSchema,
164
+ createdAt: z.number(),
165
+ approval: ChatRequestApprovalSchema.optional(),
166
+ question: z.object({ questions: z.array(ChatQuestionSchema), async: z.boolean().optional() }).optional()
167
+ });
168
+ export const ChatInfoSchema = z.object({
169
+ chatId: ChatIdSchema,
170
+ provider: AgentKindSchema,
171
+ // The account of the provider the CLI runs under; absent is the provider's default account.
172
+ account: ProviderAccountIdSchema.optional(),
173
+ cwd: z.string(),
174
+ // Set once the CLI announced itself; what a terminal node needs for `--resume`.
175
+ agentSessionId: z.string().nullable(),
176
+ // The model the CLI reported, which can differ from the selection (aliases, reroutes).
177
+ model: z.string().nullable(),
178
+ selection: ModelSelectionSchema,
179
+ runtimeMode: RuntimeModeSchema,
180
+ // The CLI's reported permissions; absent until it confirms them for the current process.
181
+ effectiveRuntimeMode: RuntimeModeSchema.optional(),
182
+ permissionMode: z.string().optional(),
183
+ status: AgentStatusSchema,
184
+ // Whether the CLI process is alive right now. A dead one is started again with `--resume` on the next send.
185
+ running: z.boolean(),
186
+ // The turn in flight, if any; items carry the same id so the client can fold work per turn.
187
+ activeTurnId: z.string().nullable(),
188
+ slashCommands: z.array(z.string()),
189
+ // What the CLI's own init frame says it will run; empty until the first message named them.
190
+ skills: z.array(z.string()).optional(),
191
+ // Messages typed while a turn ran, in the order they go out once it settles.
192
+ queue: z.array(ChatQueuedMessageSchema).optional(),
193
+ queuePaused: z.boolean().optional(),
194
+ background: z.array(ChatBackgroundTaskSchema).optional(),
195
+ // Whether a subagent or workflow of the CLI's own still runs in the background; absent reads as none.
196
+ delegating: z.boolean().optional(),
197
+ usage: ChatUsageSchema,
198
+ // The name the CLI gave the session, when it gives one; a node that nobody named takes it.
199
+ suggestedTitle: SuggestedTitleSchema.optional(),
200
+ // A chat that runs an edit inline in an editor: no list shows it, and a client that lists chats skips it.
201
+ hidden: z.boolean().optional(),
202
+ // The chat this one was forked from and the turn it continues after; absent on a chat nobody forked.
203
+ forkOf: z.object({ chatId: ChatIdSchema, turnId: z.string().min(1), at: z.number() }).optional(),
204
+ // This chat's own switch for being taken up again after a limit; absent follows the machine's `resumeAtReset`.
205
+ resumeAtReset: z.boolean().optional(),
206
+ // The limit the last turn stopped on, until the next turn opens: what a header shows without the thread.
207
+ limit: ChatTurnLimitSchema.optional(),
208
+ // When the daemon takes the chat up again on its own, after the limit its last turn stopped on; absent while nothing is owed.
209
+ resumeAt: z.number().optional(),
210
+ // What the chat waits on a person for, oldest first and at most `CHAT_REQUEST_LIMITS.perChat`; absent while nothing waits.
211
+ requests: z.array(ChatRequestSummarySchema).optional(),
212
+ createdAt: z.number()
213
+ });
214
+ // Where a skill was found: the person's own folder, the chat's folder, or a plugin.
215
+ export const ChatSkillSourceSchema = z.enum(['user', 'project', 'plugin']);
216
+ export const ChatSkillSchema = z.object({
217
+ name: z.string().min(1),
218
+ description: z.string(),
219
+ source: ChatSkillSourceSchema
220
+ });
221
+ export const SkillsListPayloadSchema = z.object({ chatId: ChatIdSchema });
222
+ export const SkillsListResultSchema = z.object({ skills: z.array(ChatSkillSchema) });
223
+ const base = {
224
+ id: z.string().min(1),
225
+ createdAt: z.number(),
226
+ turnId: z.string().nullable()
227
+ };
228
+ export const ChatUserItemSchema = z.object({
229
+ ...base,
230
+ kind: z.literal('user'),
231
+ text: z.string(),
232
+ // Files the person picked with `@`; the paths also sit in the text, this is what the row highlights.
233
+ mentions: z.array(z.string()).optional(),
234
+ // Skills the person picked with `$`; the names also sit in the text, this is what the row chips.
235
+ skills: z.array(z.string()).optional(),
236
+ // Chats of the same project the person picked with `@`. Only their ids travel; the agent reads them itself.
237
+ chats: z.array(z.string()).optional(),
238
+ attachments: z.array(ChatAttachmentSchema).optional()
239
+ });
240
+ export const ChatAssistantItemSchema = z.object({
241
+ ...base,
242
+ kind: z.literal('assistant'),
243
+ text: z.string(),
244
+ streaming: z.boolean(),
245
+ // Set for text a subagent wrote, with the id of the Agent call that spawned it.
246
+ parentToolUseId: z.string().nullable().optional()
247
+ });
248
+ // One stretch of the model thinking out loud before it answers: Claude's thinking blocks, Codex's
249
+ // reasoning summaries. Consecutive blocks are one item, so the timeline has one row per stretch.
250
+ export const ChatThinkingItemSchema = z.object({
251
+ ...base,
252
+ kind: z.literal('thinking'),
253
+ text: z.string(),
254
+ streaming: z.boolean(),
255
+ // When the stretch ended, so the row can say how long it took; null while it is still running.
256
+ endedAt: z.number().nullable()
257
+ });
258
+ export const ChatToolStateSchema = z.enum(['running', 'done', 'error']);
259
+ // What is known about a tool call while it runs; absent until the CLI reports something.
260
+ export const ChatToolProgressSchema = z.object({
261
+ // Derived from the CLI's `elapsed_time_seconds`, so the client can count on from here; null when only the description came.
262
+ startedAt: z.number().nullable(),
263
+ // What the CLI says the call is doing (Claude Code's `task_started` frame), when it said so.
264
+ description: z.string().nullable(),
265
+ // Output seen so far, for a provider that streams it; the tool's `output` replaces it when the call settles.
266
+ output: z.string().nullable()
267
+ });
268
+ // One file a tool call changed, as the CLI reports it; `diff` is unified text for a provider that
269
+ // gives one and empty for a provider whose edits only carry the text before and after.
270
+ export const ChatFileChangeSchema = z.object({
271
+ path: z.string(),
272
+ kind: z.enum(['add', 'update', 'delete']),
273
+ diff: z.string()
274
+ });
275
+ export const ChatSubagentStatusSchema = z.enum(['running', 'done', 'failed']);
276
+ export const ChatWorkflowPhaseSchema = z.object({
277
+ index: z.number().int(),
278
+ title: z.string()
279
+ });
280
+ // One agent a Claude workflow started, as its latest progress report has it.
281
+ export const ChatWorkflowAgentSchema = z.object({
282
+ // Its place in the order the script started agents, which is what the CLI keys it on.
283
+ index: z.number().int(),
284
+ label: z.string(),
285
+ // The phase it runs in; null for an agent the script started outside any phase.
286
+ phaseIndex: z.number().int().nullable(),
287
+ // Names its transcript beside the session; null while it still waits for its turn to start.
288
+ agentId: z.string().nullable(),
289
+ status: ChatSubagentStatusSchema,
290
+ startedAt: z.number().nullable(),
291
+ durationMs: z.number().int().nonnegative().nullable(),
292
+ lastTool: z.string().nullable()
293
+ });
294
+ // What a Workflow call runs: every phase the script announced, and the agents it started so far.
295
+ export const ChatWorkflowSchema = z.object({
296
+ name: z.string().nullable(),
297
+ taskId: z.string().optional(),
298
+ phases: z.array(ChatWorkflowPhaseSchema),
299
+ agents: z.array(ChatWorkflowAgentSchema),
300
+ lastProgressAt: z.number().optional(),
301
+ stalledAt: z.number().optional()
302
+ });
303
+ // A workflow's agent has no call of its own, so `chat.subagent` names it by its agent id under this prefix.
304
+ const WORKFLOW_AGENT_REF = 'workflow-agent:';
305
+ export function workflowAgentRef(agentId) {
306
+ return `${WORKFLOW_AGENT_REF}${agentId}`;
307
+ }
308
+ export function workflowAgentIdOf(ref) {
309
+ return ref.startsWith(WORKFLOW_AGENT_REF) ? ref.slice(WORKFLOW_AGENT_REF.length) || null : null;
310
+ }
311
+ export const ChatToolItemSchema = z.object({
312
+ ...base,
313
+ kind: z.literal('tool'),
314
+ toolUseId: z.string(),
315
+ name: z.string(),
316
+ input: z.unknown(),
317
+ output: z.string().nullable(),
318
+ state: ChatToolStateSchema,
319
+ // Set for a tool call made by a subagent, with the id of the Task call that spawned it.
320
+ parentToolUseId: z.string().nullable(),
321
+ progress: ChatToolProgressSchema.optional(),
322
+ changes: z.array(ChatFileChangeSchema).optional(),
323
+ // Set on a Workflow call once the CLI reports what the workflow runs, replaced whole by every report.
324
+ workflow: ChatWorkflowSchema.optional()
325
+ });
326
+ export const ChatSubagentUsageSchema = z.object({
327
+ totalTokens: z.number().int().nonnegative(),
328
+ toolUses: z.number().int().nonnegative(),
329
+ durationMs: z.number().int().nonnegative()
330
+ });
331
+ /*
332
+ * One agent the agent delegated to, foreground or background. Its own work stays in the thread as
333
+ * ordinary items that carry `parentToolUseId`, so streaming keeps working; the timeline gathers
334
+ * them under this row. `result` is the report it ended with, as markdown.
335
+ */
336
+ export const ChatSubagentItemSchema = z.object({
337
+ ...base,
338
+ kind: z.literal('subagent'),
339
+ // The id of the Agent call that spawned it, which is what every later frame about it names.
340
+ toolUseId: z.string(),
341
+ description: z.string(),
342
+ subagentType: z.string().nullable(),
343
+ // The model it runs on: the one its call asked for, until the agent's own first answer names it.
344
+ model: z.string().optional(),
345
+ prompt: z.string().nullable(),
346
+ // Whether it runs beside the turn instead of blocking it, so the turn can end before it does.
347
+ background: z.boolean(),
348
+ status: ChatSubagentStatusSchema,
349
+ startedAt: z.number(),
350
+ finishedAt: z.number().nullable(),
351
+ // What the CLI says it is doing while it runs, and what it says came of it once it settled.
352
+ summary: z.string().nullable(),
353
+ result: z.string().nullable(),
354
+ usage: ChatSubagentUsageSchema.nullable(),
355
+ // The tool it reached for last, for the line while it is still running.
356
+ lastTool: z.string().nullable(),
357
+ // The CLI's own transcript of the run, when it wrote one.
358
+ outputFile: z.string().optional(),
359
+ // Set when it did more than the thread keeps; what is there is the beginning of its work.
360
+ itemsTruncated: z.boolean(),
361
+ // Where the CLI keeps this subagent's own conversation; set once the daemon found it.
362
+ native: z.object({ agentId: z.string().optional(), threadId: z.string().optional() }).optional(),
363
+ // Who opened it: the CLI with its own tool, or a verb with `--task` that made a node; absent is `native`.
364
+ origin: z.enum(['native', 'ruimte']).optional(),
365
+ // The node a `--task` opened, whose own conversation this row stands for.
366
+ childId: z.string().optional(),
367
+ // Set for an agent a subagent opened, with the id of that subagent's own Agent call; its row hangs under that one.
368
+ parentToolUseId: z.string().optional()
369
+ });
370
+ export const ChatApprovalDecisionSchema = z.enum(['pending', 'allow', 'allow-always', 'deny', 'cancelled']);
371
+ export const ChatApprovalItemSchema = z.object({
372
+ ...base,
373
+ kind: z.literal('approval'),
374
+ requestId: z.string(),
375
+ toolUseId: z.string().nullable(),
376
+ toolName: z.string(),
377
+ input: z.unknown(),
378
+ description: z.string().nullable(),
379
+ // Whether the CLI offered a rule that would let this pass next time.
380
+ canAllowAlways: z.boolean(),
381
+ allowAlways: z.object({ label: z.string(), description: z.string() }).optional(),
382
+ decision: ChatApprovalDecisionSchema
383
+ });
384
+ export const ChatQuestionItemSchema = z.object({
385
+ ...base,
386
+ kind: z.literal('question'),
387
+ requestId: z.string(),
388
+ questions: z.array(ChatQuestionSchema).min(1),
389
+ // Set when the CLI goes on while it waits, which is the only kind that may be dismissed.
390
+ async: z.boolean().optional(),
391
+ // Keyed by question id; null while the person has not answered.
392
+ answers: z.record(z.string(), z.string()).nullable(),
393
+ state: z.enum(['pending', 'answered', 'cancelled', 'dismissed'])
394
+ });
395
+ // One file of a turn's checkpoint diff: the working tree against the tree the turn started from.
396
+ export const ChatCheckpointFileSchema = z.object({
397
+ path: z.string(),
398
+ kind: z.enum(['add', 'update', 'delete']),
399
+ added: z.number().int().nonnegative(),
400
+ deleted: z.number().int().nonnegative(),
401
+ // The unified diff of this file; empty when `omitted` says why there is none.
402
+ diff: z.string(),
403
+ omitted: z.enum(['binary', 'too-large']).optional()
404
+ });
405
+ export const ChatCheckpointDiffSchema = z.object({
406
+ files: z.array(ChatCheckpointFileSchema),
407
+ // Set when more files changed than the list carries.
408
+ truncated: z.boolean()
409
+ });
410
+ export const ChatTurnItemSchema = z.object({
411
+ ...base,
412
+ kind: z.literal('turn'),
413
+ state: z.enum(['running', 'done', 'aborted', 'error']),
414
+ // Who started the turn. Absent means the person did, which is what every turn written before this field was.
415
+ origin: z.enum(['user', 'agent']).optional(),
416
+ // What the CLI woke up about (the summary of a background task that settled); only an agent turn has one.
417
+ label: z.string().optional(),
418
+ // The Agent call the CLI woke up about, so the header can point at the subagent row it belongs to.
419
+ taskToolUseId: z.string().optional(),
420
+ endedAt: z.number().nullable(),
421
+ costUsd: z.number().nonnegative(),
422
+ // The git tree of the chat's folder when the turn started; absent outside a repository.
423
+ checkpoint: z.string().optional(),
424
+ // What the working tree holds against that checkpoint, taken when the turn settled.
425
+ checkpointDiff: ChatCheckpointDiffSchema.optional(),
426
+ // How many CLI processes worked on this turn; absent is one, which is every turn before this field.
427
+ attempt: z.number().int().positive().optional(),
428
+ // A written resume attempt whose CLI has not accepted its prompt yet; retries reuse it.
429
+ resumePending: z.boolean().optional(),
430
+ // Kept until the CLI acknowledges the wake prompt, so a saved turn cannot consume an unsent result.
431
+ deliveryPending: z.boolean().optional(),
432
+ // The tasks whose results woke the chat for this turn; only a turn the daemon opened carries them.
433
+ taskIds: z.array(z.string()).optional(),
434
+ // The nodes whose messages woke the chat for this turn, which is where waking on a message stops: a turn with one wakes nobody.
435
+ messageFrom: z.array(z.string()).optional(),
436
+ // The CLI's own name for where this turn ended, which is what a fork after this turn is cut at.
437
+ native: z.object({ turnId: z.string().optional(), lastUuid: z.string().optional() }).optional(),
438
+ // The git tree of the chat's folder when the turn settled: what a fork after this turn starts its files from.
439
+ checkpointAfter: z.string().optional(),
440
+ // Set on the turn a fork writes a summary in: the chat it is for, which gets the last answer of the turn.
441
+ summaryFor: ChatIdSchema.optional(),
442
+ // Set on a turn that ended in an error because of a limit rather than a mistake.
443
+ limit: ChatTurnLimitSchema.optional()
444
+ });
445
+ /* What a turn a restart could not take up again ends with, so a client can tell it from a turn a person stopped. */
446
+ const NOT_RESUMED_PREFIX = 'This turn could not be resumed after the machine restarted: ';
447
+ export function notResumedNote(reason) {
448
+ return `${NOT_RESUMED_PREFIX}${reason}`;
449
+ }
450
+ /* Whether the machine ended this aborted turn rather than a person: the daemon leaves its warning note in the turn. */
451
+ export function abortedByMachine(turn, items) {
452
+ return (turn.state === 'aborted' &&
453
+ items.some((item) => item.kind === 'note' && item.turnId === turn.id && item.level === 'warning' && item.text?.startsWith(NOT_RESUMED_PREFIX) === true));
454
+ }
455
+ export const ChatNoteItemSchema = z.object({
456
+ ...base,
457
+ kind: z.literal('note'),
458
+ level: z.enum(['info', 'warning', 'error']),
459
+ text: z.string(),
460
+ // The chat a delivered summary came from, so a client can offer to open it.
461
+ from: ChatIdSchema.optional()
462
+ });
463
+ export const ChatCompactionItemSchema = z.object({
464
+ ...base,
465
+ kind: z.literal('compaction'),
466
+ preTokens: z.number().int().nonnegative().nullable()
467
+ });
468
+ /*
469
+ * Never a new member here, and never a new value in an enum a chat or a push already carries: the
470
+ * iPhone app validates `chat.attach` and `chat.history` whole, so one item it does not know rejects
471
+ * the entire conversation. Add optional fields instead.
472
+ */
473
+ export const ChatItemSchema = z.discriminatedUnion('kind', [
474
+ ChatUserItemSchema,
475
+ ChatAssistantItemSchema,
476
+ ChatThinkingItemSchema,
477
+ ChatToolItemSchema,
478
+ ChatSubagentItemSchema,
479
+ ChatApprovalItemSchema,
480
+ ChatQuestionItemSchema,
481
+ ChatTurnItemSchema,
482
+ ChatNoteItemSchema,
483
+ ChatCompactionItemSchema
484
+ ]);
485
+ // Events apply after the `chat.attach` snapshot; item ids make upserts and streamed deltas replayable.
486
+ export const ChatEventSchema = z.discriminatedUnion('type', [
487
+ z.object({ type: z.literal('item'), item: ChatItemSchema, historyIndex: z.number().int().nonnegative().optional() }),
488
+ z.object({ type: z.literal('delta'), itemId: z.string(), text: z.string() }),
489
+ z.object({ type: z.literal('info'), info: ChatInfoSchema }),
490
+ z.object({ type: z.literal('reset'), info: ChatInfoSchema, items: z.array(ChatItemSchema) })
491
+ ]);
492
+ export const ChatEventEnvelopeSchema = z.object({
493
+ chatId: ChatIdSchema,
494
+ event: ChatEventSchema,
495
+ // The place of this event in the chat's stream, for `since` on the next attach.
496
+ seq: z.number().int().positive().optional()
497
+ });
498
+ export const ChatCreatePayloadSchema = z.object({
499
+ chatId: ChatIdSchema,
500
+ provider: AgentKindSchema.optional(),
501
+ account: ProviderAccountIdSchema.optional(),
502
+ cwd: z.string().optional(),
503
+ // A CLI session to continue, for a chat opened from a terminal that ran the agent.
504
+ resume: z.string().optional(),
505
+ selection: ModelSelectionSchema.optional(),
506
+ runtimeMode: RuntimeModeSchema.optional()
507
+ });
508
+ export const ChatConfigurePayloadSchema = z.object({
509
+ chatId: ChatIdSchema,
510
+ // Only an account that can continue this chat's conversation: the same provider and the same transcript folder.
511
+ account: ProviderAccountIdSchema.optional(),
512
+ selection: ModelSelectionSchema.optional(),
513
+ runtimeMode: RuntimeModeSchema.optional(),
514
+ resumeAtReset: z.boolean().optional()
515
+ });
516
+ /*
517
+ * The composer preference of this client, for a chat the daemon starts with no client mounting it.
518
+ * One socket's answer, dropped with it. Among the clients connected the newest `changedAt` wins, so
519
+ * a client that reconnects with an older pick does not override a fresher one made elsewhere.
520
+ */
521
+ export const ChatPreferencesPayloadSchema = z.object({
522
+ runtimeMode: RuntimeModeSchema.optional(),
523
+ // The mode a terminal agent node starts in, for the terminals the daemon starts on its own.
524
+ terminalRuntimeMode: RuntimeModeSchema.optional(),
525
+ // Per provider, because a model slug only means something in its own CLI's catalog.
526
+ selections: z.partialRecord(AgentKindSchema, ModelSelectionSchema).optional(),
527
+ // The account last picked per provider.
528
+ accounts: z.partialRecord(AgentKindSchema, ProviderAccountIdSchema).optional(),
529
+ // When the person last changed it, in milliseconds since the epoch; absent is older than any pick.
530
+ changedAt: z.number().nonnegative().optional()
531
+ });
532
+ export const ChatTargetPayloadSchema = z.object({ chatId: ChatIdSchema });
533
+ // Without `force` a chat in the middle of a turn is refused, so a person is asked before that turn is thrown away.
534
+ export const ChatClearPayloadSchema = ChatTargetPayloadSchema.extend({ force: z.boolean().optional() });
535
+ // With `subagents` the stop also ends every agent the chat opened and marks its CLI's own subagents stopped; the chat stays.
536
+ export const ChatCancelPayloadSchema = ChatTargetPayloadSchema.extend({ subagents: z.boolean().optional() });
537
+ export const ChatAttachPayloadSchema = ChatTargetPayloadSchema.extend({
538
+ historyLimit: z.number().int().min(1).max(100).optional(),
539
+ // The last seq this client saw; honored when the daemon still holds everything after it.
540
+ since: z.number().int().nonnegative().optional()
541
+ });
542
+ export const ChatHistoryPayloadSchema = ChatTargetPayloadSchema.extend({
543
+ cursor: z.string().min(1).max(128),
544
+ limit: z.number().int().min(1).max(100).optional()
545
+ });
546
+ export const ChatHistoryPageSchema = z.object({
547
+ start: z.number().int().nonnegative(),
548
+ cursor: z.string().nullable()
549
+ });
550
+ export const ChatHistoryResultSchema = z.object({ items: z.array(ChatItemSchema), history: ChatHistoryPageSchema });
551
+ export const ChatSubagentPayloadSchema = ChatTargetPayloadSchema.extend({
552
+ // The call that spawned it: a row of the chat's own thread, or of a subagent's conversation; a workflow's agent by `workflowAgentRef`.
553
+ toolUseId: z.string().min(1).max(256),
554
+ cursor: z.string().min(1).max(256).optional(),
555
+ limit: z.number().int().min(1).max(100).optional(),
556
+ // True keeps this client told while the conversation grows, false lets go; absent leaves it as it was.
557
+ watch: z.boolean().optional()
558
+ });
559
+ export const ChatStopSubagentPayloadSchema = ChatTargetPayloadSchema.extend({
560
+ // A running row of the chat's own thread: a task's node is stopped, a subagent of the CLI's own only marked.
561
+ toolUseId: z.string().min(1).max(256)
562
+ });
563
+ export const ChatStopTaskPayloadSchema = ChatTargetPayloadSchema.extend({ taskId: z.string().min(1).max(256) });
564
+ // `start` is the place in the conversation for a source that numbers it; a Codex thread pages by its own cursor only.
565
+ export const ChatSubagentPageSchema = z.object({
566
+ start: z.number().int().nonnegative().optional(),
567
+ cursor: z.string().nullable()
568
+ });
569
+ export const ChatSubagentSourceSchema = z.enum(['claude-transcript', 'codex-thread']);
570
+ export const ChatSubagentResultSchema = z.object({
571
+ items: z.array(ChatItemSchema),
572
+ history: ChatSubagentPageSchema,
573
+ source: ChatSubagentSourceSchema,
574
+ context: z.object({ provider: AgentKindSchema, cwd: z.string(), chatId: ChatIdSchema.optional() }).optional(),
575
+ // Whether the subagent is still writing, so a client knows to keep reading.
576
+ live: z.boolean()
577
+ });
578
+ /*
579
+ * The status of one chat, sent to every client on this machine rather than only to the ones
580
+ * attached to it. A thread's events are only worth streaming to whoever reads them, but what a chat
581
+ * is doing belongs to the whole project: a node waiting on a person has to say so on a view nobody
582
+ * has open. Terminals have said this all along through `session.status`.
583
+ */
584
+ export const ChatStatusEventSchema = z.object({ chatId: ChatIdSchema, info: ChatInfoSchema });
585
+ // Carries nothing of the conversation: a client that holds it asks for the newest page again.
586
+ export const ChatSubagentChangedEventSchema = z.object({ chatId: ChatIdSchema, toolUseId: z.string() });
587
+ export const CHAT_BOOKMARK_LIMITS = {
588
+ name: 120,
589
+ excerpt: 160,
590
+ perChat: 200
591
+ };
592
+ /*
593
+ * A message a person marked to come back to. It hangs on the item's id, never on a place in the
594
+ * thread, and lives beside the chat in the host data directory, so every client of the chat sees the same.
595
+ */
596
+ export const ChatBookmarkSchema = z.object({
597
+ itemId: z.string().min(1),
598
+ // Absent while nobody named it; a list shows the excerpt instead.
599
+ name: z.string().max(CHAT_BOOKMARK_LIMITS.name).optional(),
600
+ // The start of the message when it was marked, so a list needs no thread to say what it points at.
601
+ excerpt: z.string().max(CHAT_BOOKMARK_LIMITS.excerpt),
602
+ createdAt: z.number()
603
+ });
604
+ export const ChatBookmarksSchema = z.array(ChatBookmarkSchema);
605
+ // Marking a message that already has a bookmark keeps it, and names it when a name comes along.
606
+ export const ChatAddBookmarkPayloadSchema = ChatTargetPayloadSchema.extend({
607
+ itemId: z.string().min(1),
608
+ name: z.string().max(CHAT_BOOKMARK_LIMITS.name).optional()
609
+ });
610
+ // An empty name takes the name away.
611
+ export const ChatRenameBookmarkPayloadSchema = ChatTargetPayloadSchema.extend({
612
+ itemId: z.string().min(1),
613
+ name: z.string().max(CHAT_BOOKMARK_LIMITS.name)
614
+ });
615
+ // A bookmark that is already gone is no refusal: another client took it away first.
616
+ export const ChatRemoveBookmarkPayloadSchema = ChatTargetPayloadSchema.extend({ itemId: z.string().min(1) });
617
+ export const ChatBookmarksResultSchema = z.object({ bookmarks: ChatBookmarksSchema });
618
+ // The whole list after every change, to every client attached to the chat.
619
+ export const ChatBookmarksEventSchema = z.object({ chatId: ChatIdSchema, bookmarks: ChatBookmarksSchema });
620
+ export const ChatAttachResultSchema = z.object({
621
+ info: ChatInfoSchema,
622
+ items: z.array(ChatItemSchema),
623
+ history: ChatHistoryPageSchema.optional(),
624
+ pending: z.array(ChatItemSchema).optional(),
625
+ seq: z.number().int().nonnegative().optional(),
626
+ // Only when `since` was honored: what happened after it, in order; `items` is then empty.
627
+ events: z.array(ChatEventSchema).optional(),
628
+ // Absent from a daemon that keeps no bookmarks.
629
+ bookmarks: ChatBookmarksSchema.optional()
630
+ });
631
+ export const ChatSendPayloadSchema = z
632
+ .object({
633
+ chatId: ChatIdSchema,
634
+ text: z.string(),
635
+ mentions: z.array(z.string().min(1)).max(64).optional(),
636
+ skills: z.array(z.string().min(1)).max(16).optional(),
637
+ chats: z.array(ChatIdSchema).max(16).optional(),
638
+ attachments: ChatAttachmentUploadsSchema.optional()
639
+ })
640
+ .refine((payload) => payload.text.trim() !== '' || (payload.attachments?.length ?? 0) > 0, { message: 'A message needs text or an attachment' });
641
+ // Daemons before completion follow-ups omit `turnId`; keeping it optional lets newer clients finish the send.
642
+ export const ChatSendResultSchema = z.object({ queued: z.boolean(), turnId: z.string().min(1).optional() });
643
+ export const ChatQueuePayloadSchema = z.object({
644
+ chatId: ChatIdSchema,
645
+ messageId: z.string().min(1)
646
+ });
647
+ // The message as it left the queue. An older daemon answers `{}` and refuses one it no longer holds.
648
+ export const ChatUnqueueResultSchema = z.object({
649
+ message: ChatQueuedMessageSchema.optional()
650
+ });
651
+ export const ChatApprovePayloadSchema = z.object({
652
+ chatId: ChatIdSchema,
653
+ requestId: z.string().min(1),
654
+ decision: z.enum(['allow', 'allow-always', 'deny']),
655
+ message: z.string().optional()
656
+ });
657
+ // Leaves an asynchronous question alone; the agent never hears about it and the item settles.
658
+ export const ChatDismissPayloadSchema = z.object({
659
+ chatId: ChatIdSchema,
660
+ itemId: z.string().min(1)
661
+ });
662
+ export const ChatAnswerPayloadSchema = z.object({
663
+ chatId: ChatIdSchema,
664
+ requestId: z.string().min(1),
665
+ answers: z.record(z.string(), z.string())
666
+ });
667
+ export const ChatTurnDiffPayloadSchema = z.object({
668
+ chatId: ChatIdSchema,
669
+ turnId: z.string().min(1)
670
+ });
671
+ // Null when the turn has no checkpoint to diff against: no repository, or git could not be read.
672
+ export const ChatTurnDiffResultSchema = z.object({ diff: ChatCheckpointDiffSchema.nullable() });
673
+ // A title of a node the fork makes; the same cap the canvas verbs hold a title to.
674
+ export const CHAT_FORK_TITLE_MAX = 120;
675
+ /*
676
+ * A new chat that goes on after `turnId`, with the history up to and including that turn. The fork of
677
+ * a node is a node beside it unless `asView` asks for a chat view of its own, listed right after the
678
+ * canvas the node stands on. The fork of a chat that is a view of its own is a view listed right after
679
+ * it, unless `viewId` names a canvas to put a node on instead.
680
+ */
681
+ export const ChatForkPayloadSchema = z.object({
682
+ chatId: ChatIdSchema,
683
+ turnId: z.string().min(1),
684
+ title: z.string().trim().min(1).max(CHAT_FORK_TITLE_MAX).optional(),
685
+ viewId: z.string().min(1).optional(),
686
+ asView: z.boolean().optional(),
687
+ // A git worktree of its own on a new branch; absent is the original's folder. The branch defaults to one named after the title.
688
+ worktree: z.object({ branch: z.string().trim().min(1).max(CHAT_FORK_TITLE_MAX).optional() }).optional(),
689
+ // With a worktree: its files as they were after the turn rather than the branch's HEAD.
690
+ filesAfterTurn: z.boolean().optional(),
691
+ // Another CLI to go on with, which gets the conversation as text; absent is the original's CLI.
692
+ provider: AgentKindSchema.optional(),
693
+ // The model of that CLI; absent is the newest composer pick for it.
694
+ selection: ModelSelectionSchema.optional(),
695
+ // The account to go on under; absent is the original's account when the fork stays with its CLI.
696
+ account: ProviderAccountIdSchema.optional()
697
+ });
698
+ /*
699
+ * `viewId` is the canvas the node landed on, or the fork's own view, whose id is `nodeId`. `edgeId`
700
+ * is null when no line could be drawn from the original: it stands on no canvas, or the fork does not.
701
+ */
702
+ export const ChatForkResultSchema = z.object({
703
+ info: ChatInfoSchema,
704
+ nodeId: z.string(),
705
+ viewId: z.string(),
706
+ edgeId: z.string().nullable(),
707
+ worktree: WorktreeSchema.optional()
708
+ });
709
+ /*
710
+ * Goes on after the last turn of a chat, which stopped on a limit, under another account of its CLI.
711
+ * An account that reads the same transcripts takes the chat over in place; any other goes on in a
712
+ * fork that gets the conversation handed over. Only ever asked by a person, never done on its own.
713
+ */
714
+ export const ChatContinueOnPayloadSchema = z.object({
715
+ chatId: ChatIdSchema,
716
+ account: ProviderAccountIdSchema
717
+ });
718
+ export const ChatContinueOnResultSchema = z.object({
719
+ // The chat that goes on: this one, or the fork.
720
+ chatId: ChatIdSchema,
721
+ // Set when it went on in a fork, which is where the client goes.
722
+ fork: ChatForkResultSchema.optional()
723
+ });
724
+ export const ChatSummarizePayloadSchema = z.object({ chatId: ChatIdSchema });
725
+ // The turn the fork writes its summary in; its last answer goes to the original once the turn ends.
726
+ export const ChatSummarizeResultSchema = z.object({ turnId: z.string() });
727
+ export const ChatForkInfoPayloadSchema = z.object({ chatId: ChatIdSchema, turnId: z.string().min(1) });
728
+ /*
729
+ * What the fork dialog asks before it offers a worktree: whether the chat's folder is in a repository,
730
+ * the branches taken there with a free one to suggest, and whether the files after that turn can
731
+ * still be put back (a tree git collected, or a turn that never had one, cannot).
732
+ */
733
+ export const ChatForkInfoResultSchema = z.object({
734
+ repository: z.boolean(),
735
+ branches: z.array(z.string()),
736
+ branch: z.string().nullable(),
737
+ filesAfterTurn: z.boolean()
738
+ });
739
+ export const ChatListResultSchema = z.object({
740
+ chats: z.array(ChatInfoSchema)
741
+ });