@parall/claude-agent 1.59.0 → 1.60.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.
@@ -1,5 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import type { DispatchInputLifecycle, GatewayLogger, RuntimeInputState } from '@parall/agent-core';
3
+ import {
4
+ type ClaudeTurnSink,
5
+ newTurnEvidence,
6
+ newTurnSink,
7
+ type TurnEvidence,
8
+ } from './turn-sink.js';
3
9
 
4
10
  export type ClaudeInputDelivery = {
5
11
  deliveryKey: string;
@@ -10,6 +16,14 @@ export type ClaudeInputDelivery = {
10
16
  terminal?: 'completed' | 'failed' | 'settled';
11
17
  reportedState?: RuntimeInputState;
12
18
  resultFailed: boolean;
19
+ /** Envelopes the stdout pump routed to this delivery; drained by its dispatch. */
20
+ sink: ClaudeTurnSink;
21
+ /** Turn-outcome evidence gathered while this delivery owned stdout. */
22
+ evidence: TurnEvidence;
23
+ /** Dispatch inactivity signal of the dispatch draining (or about to drain) this delivery. */
24
+ noteActivity?: () => void;
25
+ /** runtime_session already pushed to this delivery's sink. */
26
+ sessionAnnounced: boolean;
13
27
  /**
14
28
  * The turn settled as usage_limit: the lane-level deferred complete will
15
29
  * re-deliver this input at retry_at without burning redrive budget. A
@@ -73,17 +87,24 @@ export class ClaudeInputRegistry {
73
87
  deliveryKey: string,
74
88
  lifecycle: DispatchInputLifecycle | undefined,
75
89
  injected: boolean,
90
+ noteActivity?: () => void,
91
+ log?: GatewayLogger,
76
92
  ): ClaudeInputDelivery {
77
93
  if (this.byKey.has(deliveryKey)) {
78
94
  throw new Error(`duplicate Claude delivery key ${deliveryKey}`);
79
95
  }
96
+ const commandUuid = randomUUID();
80
97
  const delivery: ClaudeInputDelivery = {
81
98
  deliveryKey,
82
- commandUuid: randomUUID(),
99
+ commandUuid,
83
100
  lifecycle,
84
101
  injected,
85
102
  drained: !injected,
86
103
  resultFailed: false,
104
+ sink: newTurnSink(`delivery ${deliveryKey}`, log),
105
+ evidence: newTurnEvidence(),
106
+ noteActivity,
107
+ sessionAnnounced: false,
87
108
  };
88
109
  this.byKey.set(deliveryKey, delivery);
89
110
  this.byCommand.set(delivery.commandUuid, delivery);
@@ -97,6 +118,9 @@ export class ClaudeInputRegistry {
97
118
  if (this.byCommand.get(delivery.commandUuid) === delivery) {
98
119
  this.byCommand.delete(delivery.commandUuid);
99
120
  }
121
+ // The sink stays open: apply() removes a drained delivery at its
122
+ // terminal BEFORE the pump pushes the terminal envelope its drain is
123
+ // waiting for. Only the pump closes sinks (process gone).
100
124
  }
101
125
 
102
126
  /**
@@ -27,9 +27,52 @@ export type ClaudeResultMeta = {
27
27
  model?: string;
28
28
  };
29
29
 
30
+ /**
31
+ * The CLI's background-work frames (`system` subtypes). Every field is
32
+ * optional except the subtype.
33
+ * Observed on 2.1.227/2.1.258: `background_tasks_changed` (REPLACE
34
+ * semantics — every live task after the change), `task_started`,
35
+ * `task_progress`, `task_updated`, `task_notification` (a task finished or
36
+ * was stopped), `status`, `session_state_changed` (env-gated).
37
+ */
38
+ export type ClaudeRuntimeTaskEvent = {
39
+ type: 'runtime_task';
40
+ subtype:
41
+ | 'background_tasks_changed'
42
+ | 'task_started'
43
+ | 'task_progress'
44
+ | 'task_updated'
45
+ | 'task_notification'
46
+ | 'status'
47
+ | 'session_state_changed';
48
+ taskId?: string;
49
+ toolUseId?: string;
50
+ description?: string;
51
+ taskType?: string;
52
+ status?: string;
53
+ summary?: string;
54
+ isBackgrounded?: boolean;
55
+ tasks?: Array<{ taskId: string; taskType?: string; description?: string; ambient?: boolean }>;
56
+ };
57
+
58
+ const RUNTIME_TASK_SUBTYPES = new Set<ClaudeRuntimeTaskEvent['subtype']>([
59
+ 'background_tasks_changed',
60
+ 'task_started',
61
+ 'task_progress',
62
+ 'task_updated',
63
+ 'task_notification',
64
+ 'status',
65
+ 'session_state_changed',
66
+ ]);
67
+
30
68
  export type ClaudeParsedEvent =
31
69
  | RuntimeEvent
32
70
  | { type: 'runtime_init'; sessionId?: string; capabilities: string[] }
71
+ | ClaudeRuntimeTaskEvent
72
+ // A root `user` frame that carries text instead of tool results (the
73
+ // CLI's own injected notices, e.g. a `<task-notification>`). Never a
74
+ // RuntimeEvent: the model's turn already carries its consequence.
75
+ | { type: 'user_text'; text: string }
33
76
  // Internal-only progress marker for a valid CLI frame whose content must
34
77
  // not be projected into the root RuntimeEvent stream (for example, nested
35
78
  // subagent output). The dispatch consumer uses it only to refresh runtime
@@ -44,6 +87,10 @@ export type ClaudeParsedEvent =
44
87
  // is not a model message — surfaced so the turn-outcome classifier can see
45
88
  // the text without it entering the RuntimeEvent stream.
46
89
  | { type: 'assistant_error'; message: string }
90
+ // `system` frame with subtype `compact_boundary`: the CLI folded history
91
+ // (a manual `/compact` — the idle auto-compact path — or its own
92
+ // auto-compact mid-turn). Evidence only; never a RuntimeEvent.
93
+ | { type: 'compact_boundary'; trigger?: string; preTokens?: number }
47
94
  // turn_end is emitted for every `result` frame (both `is_error: true` and
48
95
  // `is_error: false`); `isError` carries the frame's status so the consumer
49
96
  // can tell a clean turn boundary from a failed one without inspecting the
@@ -169,21 +216,49 @@ export async function* parseClaudeStreamJson(
169
216
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
170
217
  const eventRecord = event as Record<string, unknown>;
171
218
 
172
- if (eventRecord.type === 'system' && eventRecord.subtype === 'init') {
173
- const sessionId = asTrimmedString(eventRecord.session_id);
174
- const capabilities = Array.isArray(eventRecord.capabilities)
175
- ? eventRecord.capabilities
176
- .map((capability) => asTrimmedString(capability))
177
- .filter((capability): capability is string => Boolean(capability))
178
- : [];
219
+ if (eventRecord.type === 'system' && eventRecord.subtype === 'compact_boundary') {
220
+ const meta =
221
+ eventRecord.compact_metadata && typeof eventRecord.compact_metadata === 'object'
222
+ ? (eventRecord.compact_metadata as Record<string, unknown>)
223
+ : undefined;
224
+ const trigger = asTrimmedString(meta?.trigger);
225
+ const preTokens = asFiniteNumber(meta?.pre_tokens);
179
226
  yield {
180
- type: 'runtime_init',
181
- ...(sessionId ? { sessionId } : {}),
182
- capabilities,
227
+ type: 'compact_boundary',
228
+ ...(trigger ? { trigger } : {}),
229
+ ...(preTokens !== undefined ? { preTokens } : {}),
183
230
  };
184
231
  continue;
185
232
  }
186
233
 
234
+ if (eventRecord.type === 'system') {
235
+ if (eventRecord.subtype === 'init') {
236
+ const sessionId = asTrimmedString(eventRecord.session_id);
237
+ const capabilities = Array.isArray(eventRecord.capabilities)
238
+ ? eventRecord.capabilities
239
+ .map((capability) => asTrimmedString(capability))
240
+ .filter((capability): capability is string => Boolean(capability))
241
+ : [];
242
+ yield {
243
+ type: 'runtime_init',
244
+ ...(sessionId ? { sessionId } : {}),
245
+ capabilities,
246
+ };
247
+ continue;
248
+ }
249
+ const subtype = asTrimmedString(eventRecord.subtype) as
250
+ | ClaudeRuntimeTaskEvent['subtype']
251
+ | undefined;
252
+ if (subtype && RUNTIME_TASK_SUBTYPES.has(subtype)) {
253
+ yield parseRuntimeTask(subtype, eventRecord);
254
+ continue;
255
+ }
256
+ // hook_started / hook_response / thinking_tokens /
257
+ // ...: valid CLI progress with no projection.
258
+ yield { type: 'runtime_activity' };
259
+ continue;
260
+ }
261
+
187
262
  if (eventRecord.type === 'command_lifecycle') {
188
263
  const commandUuid = asTrimmedString(eventRecord.command_uuid);
189
264
  const state = asTrimmedString(eventRecord.state);
@@ -277,7 +352,17 @@ export async function* parseClaudeStreamJson(
277
352
  message && typeof message === 'object'
278
353
  ? (message as { content?: unknown }).content
279
354
  : undefined;
355
+ if (typeof content === 'string') {
356
+ const text = content.trim();
357
+ if (text) yield { type: 'user_text', text };
358
+ continue;
359
+ }
280
360
  if (!Array.isArray(content)) continue;
361
+ if (!content.some((block) => (block as { type?: unknown })?.type === 'tool_result')) {
362
+ const text = stringifyContent(content).trim();
363
+ if (text) yield { type: 'user_text', text };
364
+ continue;
365
+ }
281
366
 
282
367
  for (const block of content) {
283
368
  if (!block || typeof block !== 'object') continue;
@@ -329,8 +414,53 @@ export async function* parseClaudeStreamJson(
329
414
  ...(numTurns !== undefined ? { numTurns } : {}),
330
415
  resultMeta: extractResultMeta(eventRecord, isError, numTurns),
331
416
  };
417
+ continue;
418
+ }
419
+
420
+ // Any other well-formed frame (rate_limit_event, stream_event, ...) is
421
+ // CLI progress: refresh activity, never project.
422
+ yield { type: 'runtime_activity' };
423
+ }
424
+ }
425
+
426
+ function parseRuntimeTask(
427
+ subtype: ClaudeRuntimeTaskEvent['subtype'],
428
+ frame: Record<string, unknown>,
429
+ ): ClaudeRuntimeTaskEvent {
430
+ const event: ClaudeRuntimeTaskEvent = { type: 'runtime_task', subtype };
431
+ const taskId = asTrimmedString(frame.task_id);
432
+ if (taskId) event.taskId = taskId;
433
+ const toolUseId = asTrimmedString(frame.tool_use_id);
434
+ if (toolUseId) event.toolUseId = toolUseId;
435
+ const description = asTrimmedString(frame.description);
436
+ if (description) event.description = description;
437
+ const taskType = asTrimmedString(frame.task_type);
438
+ if (taskType) event.taskType = taskType;
439
+ const status =
440
+ asTrimmedString(frame.status) ??
441
+ asTrimmedString((frame.patch as { status?: unknown } | undefined)?.status) ??
442
+ asTrimmedString(frame.state);
443
+ if (status) event.status = status;
444
+ const summary = asTrimmedString(frame.summary);
445
+ if (summary) event.summary = summary;
446
+ if (typeof frame.is_backgrounded === 'boolean') event.isBackgrounded = frame.is_backgrounded;
447
+ if (Array.isArray(frame.tasks)) {
448
+ event.tasks = [];
449
+ for (const task of frame.tasks) {
450
+ if (!task || typeof task !== 'object') continue;
451
+ const record = task as Record<string, unknown>;
452
+ const id = asTrimmedString(record.task_id);
453
+ if (!id) continue;
454
+ const entry: NonNullable<ClaudeRuntimeTaskEvent['tasks']>[number] = { taskId: id };
455
+ const entryType = asTrimmedString(record.task_type);
456
+ if (entryType) entry.taskType = entryType;
457
+ const entryDescription = asTrimmedString(record.description);
458
+ if (entryDescription) entry.description = entryDescription;
459
+ if (record.ambient === true) entry.ambient = true;
460
+ event.tasks.push(entry);
332
461
  }
333
462
  }
463
+ return event;
334
464
  }
335
465
 
336
466
  /** Snapshot the result frame's discriminator + usage fields (all optional). */