@hunterzhu/pulse-runtime 0.1.5 → 0.1.7

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,16 +1,21 @@
1
- import type { AgentRecord, ArtifactRef, LaneRecord, LLMRequestProjection, PrivacyLabel, PrivacyTaint, ResultRef, RuntimeState, JsonValue, ContextDelta } from '../core/types.js';
1
+ import type { AgentRecord, ArtifactRef, ConversationMessage, LaneRecord, LLMRequestProjection, PrivacyLabel, PrivacyTaint, ResultRef, RuntimeState, JsonValue, ContextDelta } from '../core/types.js';
2
2
  export interface ContextBuildInput {
3
3
  agent: AgentRecord;
4
4
  lane: LaneRecord;
5
5
  resultRefs?: ResultRef[];
6
6
  artifactRefs?: ArtifactRef[];
7
7
  eventIds?: string[];
8
+ conversation?: ConversationMessage[];
8
9
  instruction: string;
9
10
  system?: string;
10
11
  policy?: JsonValue;
11
12
  tools?: JsonValue;
12
13
  toolSetId: string;
13
14
  }
15
+ export declare const MAX_DSL_INSTRUCTION_BYTES = 2048;
16
+ /** Keep large tool results from crowding out the actual task and conversation. */
17
+ export declare const MAX_INLINE_RESULT_BYTES = 4096;
18
+ export declare function assertDslInstructionSize(value: string): string;
14
19
  export declare class ContextBuilder {
15
20
  private readonly state;
16
21
  readonly version = "1";
@@ -8,6 +8,35 @@ function stable(value) {
8
8
  return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;
9
9
  }
10
10
  function hash(value) { return createHash('sha256').update(stable(value)).digest('hex'); }
11
+ export const MAX_DSL_INSTRUCTION_BYTES = 2_048;
12
+ /** Keep large tool results from crowding out the actual task and conversation. */
13
+ export const MAX_INLINE_RESULT_BYTES = 4_096;
14
+ function utf8Prefix(value, maxBytes) {
15
+ return Buffer.from(value, 'utf8').subarray(0, maxBytes).toString('utf8');
16
+ }
17
+ function projectResultValue(result) {
18
+ const value = result.value ?? null;
19
+ const originalBytes = Buffer.byteLength(JSON.stringify(value), 'utf8');
20
+ if (originalBytes <= MAX_INLINE_RESULT_BYTES)
21
+ return { value, summarized: false };
22
+ if (result.summary !== undefined)
23
+ return { value: result.summary, summarized: true, originalBytes };
24
+ return {
25
+ value: {
26
+ truncated: true,
27
+ originalBytes,
28
+ preview: utf8Prefix(JSON.stringify(value), MAX_INLINE_RESULT_BYTES),
29
+ note: 'The full result is retained by the runtime. Use this preview. Do not re-run the producing tool.',
30
+ },
31
+ summarized: true,
32
+ originalBytes,
33
+ };
34
+ }
35
+ export function assertDslInstructionSize(value) {
36
+ if (Buffer.byteLength(value, 'utf8') > MAX_DSL_INSTRUCTION_BYTES)
37
+ throw Object.assign(new Error('Instruction exceeds the 2 KB DSL limit.'), { code: 'INSTRUCTION_TOO_LARGE', retryable: false });
38
+ return value;
39
+ }
11
40
  export class ContextBuilder {
12
41
  state;
13
42
  version = '1';
@@ -15,8 +44,7 @@ export class ContextBuilder {
15
44
  this.state = state;
16
45
  }
17
46
  build(input) {
18
- if (input.instruction.length > 2048)
19
- throw new Error('INSTRUCTION_TOO_LARGE');
47
+ assertDslInstructionSize(input.instruction);
20
48
  const global = input.agent.globalVersions.get(input.lane.contextSnapshotVersion);
21
49
  if (global === undefined)
22
50
  throw new Error('UNKNOWN_CONTEXT_VERSION');
@@ -55,15 +83,20 @@ export class ContextBuilder {
55
83
  ...effectiveResults.flatMap((item) => (item.result.privacyTaints ?? []).map((taint) => ({ path: [item.result.id, ...taint.path], privacy: taint.privacy }))),
56
84
  ...effectiveArtifacts.flatMap((item) => (item.artifact.privacyTaints ?? []).map((taint) => ({ path: [item.artifact.ref, ...taint.path], privacy: taint.privacy }))),
57
85
  ];
58
- const contextSpec = { globalSnapshotVersion: input.lane.contextSnapshotVersion, laneSnapshotVersion: input.lane.context.version, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: input.eventIds ?? [], toolSetId: input.toolSetId, instruction: input.instruction, privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
86
+ const conversation = input.conversation ?? [];
87
+ const contextSpec = { globalSnapshotVersion: input.lane.contextSnapshotVersion, laneSnapshotVersion: input.lane.context.version, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: input.eventIds ?? [], toolSetId: input.toolSetId, instruction: input.instruction, ...(conversation.length ? { conversation: structuredClone(conversation) } : {}), privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
59
88
  const prefixBlocks = [
60
89
  { kind: 'system', content: input.system ?? '' },
61
90
  { kind: 'policy', content: input.policy ?? {} },
62
91
  { kind: 'tools', content: input.tools ?? {} },
63
92
  { kind: 'global', content: global },
93
+ ...(conversation.length ? [{ kind: 'conversation', content: structuredClone(conversation) }] : []),
64
94
  { kind: 'history', content: input.lane.context.history.map((record) => ({ seq: record.seq, ...(record.effectId === undefined ? {} : { effectId: record.effectId }), instruction: record.instruction, resultRefs: record.resultRefs, ...(record.resultSelection === undefined ? {} : { resultSelection: record.resultSelection }), ...(record.result === undefined ? {} : { result: record.result }), ...(record.findings === undefined ? {} : { findings: record.findings }), output: record.output, privacy: record.privacy, ...(record.privacyTaints === undefined ? {} : { privacyTaints: record.privacyTaints }) })) },
65
95
  ];
66
- const blocks = [...prefixBlocks, { kind: 'lane', content: input.lane.context.state }, { kind: 'events', content: input.eventIds ?? [] }, { kind: 'results', content: results.map((result) => ({ id: result.id, value: result.value ?? null, ...(result.privacyTaints === undefined ? {} : { privacyTaints: result.privacyTaints.map((taint) => ({ path: [...taint.path], privacy: taint.privacy })) }) })) }, { kind: 'artifacts', content: artifacts.map((artifact) => ({ ref: artifact.ref, mediaType: artifact.mediaType, sizeBytes: artifact.sizeBytes, contentHash: artifact.contentHash })) }, { kind: 'instruction', content: input.instruction }];
96
+ const blocks = [...prefixBlocks, { kind: 'lane', content: input.lane.context.state }, { kind: 'events', content: input.eventIds ?? [] }, { kind: 'results', content: results.map((result) => {
97
+ const projected = projectResultValue(result);
98
+ return { id: result.id, value: projected.value, ...(projected.summarized ? { summarized: true, ...(projected.originalBytes === undefined ? {} : { originalBytes: projected.originalBytes }) } : {}), ...(result.privacyTaints === undefined ? {} : { privacyTaints: result.privacyTaints.map((taint) => ({ path: [...taint.path], privacy: taint.privacy })) }) };
99
+ }) }, { kind: 'artifacts', content: artifacts.map((artifact) => ({ ref: artifact.ref, mediaType: artifact.mediaType, sizeBytes: artifact.sizeBytes, contentHash: artifact.contentHash })) }, { kind: 'instruction', content: input.instruction }];
67
100
  return { contextSpec, blocks, prefixHash: hash(prefixBlocks), projectionHash: hash(blocks), builderVersion: this.version, policyVersion: '1', toolSetVersion: input.toolSetId, privacy, privacyRefs, ...(privacyTaints.length ? { privacyTaints } : {}) };
68
101
  }
69
102
  }
@@ -57,6 +57,8 @@ export interface FactInboxSnapshot<T extends JsonValue = JsonValue> {
57
57
  nextSeq: number;
58
58
  seen: string[];
59
59
  queue: FactEnvelope<T>[];
60
+ /** Number of queued urgent entries at the head of `queue` (v2+). */
61
+ urgentCount?: number;
60
62
  dedupeLedger?: FactInboxDedupeLedgerSnapshot;
61
63
  }
62
64
  export declare class FactInbox<T extends JsonValue = JsonValue> {
@@ -64,13 +66,17 @@ export declare class FactInbox<T extends JsonValue = JsonValue> {
64
66
  private readonly seen;
65
67
  private nextSeq;
66
68
  private archivedThrough;
69
+ private urgentCount;
67
70
  private dedupeArchive;
68
71
  constructor(options?: {
69
72
  dedupeArchive?: FactInboxDedupeArchive;
70
73
  });
71
74
  /** Attach a backend-provided archive before the first compaction. */
72
75
  attachDedupeArchive(archive: FactInboxDedupeArchive): void;
76
+ private enqueueInternal;
73
77
  enqueue(fact: T, eventId: string): FactEnvelope<T> | undefined;
78
+ /** Insert a host control fact ahead of already queued background work. */
79
+ enqueueUrgent(fact: T, eventId: string): FactEnvelope<T> | undefined;
74
80
  drain(limit?: number): FactEnvelope<T>[];
75
81
  get size(): number;
76
82
  get dedupeWatermark(): number;
@@ -14,6 +14,7 @@ export class FactInbox {
14
14
  seen = new Map();
15
15
  nextSeq = 1;
16
16
  archivedThrough = 0;
17
+ urgentCount = 0;
17
18
  dedupeArchive;
18
19
  constructor(options = {}) {
19
20
  this.dedupeArchive = options.dedupeArchive;
@@ -26,18 +27,28 @@ export class FactInbox {
26
27
  throw new Error('FACT_INBOX_DEDUPE_ARCHIVE_ALREADY_COMPACTED');
27
28
  this.dedupeArchive = archive;
28
29
  }
29
- enqueue(fact, eventId) {
30
+ enqueueInternal(fact, eventId, urgent) {
30
31
  if (!eventId || this.seen.has(eventId) || this.dedupeArchive?.contains(eventId))
31
32
  return undefined;
32
33
  const envelope = { eventId, receivedSeq: this.nextSeq++, fact: structuredClone(fact) };
33
34
  this.seen.set(eventId, envelope.receivedSeq);
34
- this.queue.push(envelope);
35
+ if (urgent) {
36
+ this.queue.splice(this.urgentCount, 0, envelope);
37
+ this.urgentCount++;
38
+ }
39
+ else
40
+ this.queue.push(envelope);
35
41
  return structuredClone(envelope);
36
42
  }
43
+ enqueue(fact, eventId) { return this.enqueueInternal(fact, eventId, false); }
44
+ /** Insert a host control fact ahead of already queued background work. */
45
+ enqueueUrgent(fact, eventId) { return this.enqueueInternal(fact, eventId, true); }
37
46
  drain(limit = Number.POSITIVE_INFINITY) {
38
47
  if (limit !== Number.POSITIVE_INFINITY && (!Number.isInteger(limit) || limit < 0))
39
48
  throw new Error('INVALID_FACT_DRAIN_LIMIT');
40
- return this.queue.splice(0, limit).map((envelope) => structuredClone(envelope));
49
+ const drained = this.queue.splice(0, limit);
50
+ this.urgentCount = Math.max(0, this.urgentCount - drained.length);
51
+ return drained.map((envelope) => structuredClone(envelope));
41
52
  }
42
53
  get size() { return this.queue.length; }
43
54
  get dedupeWatermark() { return this.archivedThrough; }
@@ -50,6 +61,7 @@ export class FactInbox {
50
61
  nextSeq: this.nextSeq,
51
62
  seen: [...this.seen.keys()],
52
63
  queue: this.queue.map((envelope) => structuredClone(envelope)),
64
+ ...(this.urgentCount === 0 ? {} : { urgentCount: this.urgentCount }),
53
65
  dedupeLedger: {
54
66
  schemaVersion: 1,
55
67
  archivedThrough: this.archivedThrough,
@@ -117,10 +129,11 @@ export class FactInbox {
117
129
  this.seen.set(eventId, receivedSeq);
118
130
  this.nextSeq = restored.nextSeq;
119
131
  this.archivedThrough = restored.archivedThrough;
132
+ this.urgentCount = restored.urgentCount;
120
133
  }
121
134
  static fromSnapshot(snapshot, options = {}) {
122
135
  const value = snapshot;
123
- if (!value || (value.schemaVersion !== 1 && value.schemaVersion !== 2) || !Number.isInteger(value.nextSeq) || value.nextSeq < 1 || !Array.isArray(value.seen) || value.seen.some((eventId) => typeof eventId !== 'string' || eventId.length === 0) || !Array.isArray(value.queue))
136
+ if (!value || (value.schemaVersion !== 1 && value.schemaVersion !== 2) || !Number.isInteger(value.nextSeq) || value.nextSeq < 1 || !Array.isArray(value.seen) || value.seen.some((eventId) => typeof eventId !== 'string' || eventId.length === 0) || !Array.isArray(value.queue) || (value.urgentCount !== undefined && (!Number.isInteger(value.urgentCount) || value.urgentCount < 0 || value.urgentCount > value.queue.length)))
124
137
  throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
125
138
  if (new Set(value.seen).size !== value.seen.length)
126
139
  throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
@@ -151,8 +164,8 @@ export class FactInbox {
151
164
  throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
152
165
  if (inbox.queue.some((candidate) => candidate.eventId === envelope.eventId || candidate.receivedSeq === envelope.receivedSeq))
153
166
  throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
154
- if (envelope.receivedSeq <= maxReceivedSeq)
155
- throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
167
+ // Urgent host facts may be inserted ahead of older entries, so queue
168
+ // order is intentionally different from received sequence order.
156
169
  const knownSeq = seen.get(envelope.eventId);
157
170
  if (knownSeq !== undefined && knownSeq !== envelope.receivedSeq)
158
171
  throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
@@ -161,14 +174,25 @@ export class FactInbox {
161
174
  inbox.queue.push({ eventId: envelope.eventId, receivedSeq: envelope.receivedSeq, fact: structuredClone(envelope.fact) });
162
175
  maxReceivedSeq = Math.max(maxReceivedSeq, envelope.receivedSeq);
163
176
  }
177
+ // Preserve the serialized queue order. Urgent entries are allowed to have
178
+ // newer receivedSeq values ahead of background entries, but each priority
179
+ // band must retain its own FIFO order. This rejects ambiguous snapshots
180
+ // instead of silently normalizing a caller-provided reorder.
181
+ const urgentCount = value.urgentCount ?? 0;
182
+ const urgentSeqs = value.queue.slice(0, urgentCount).map((entry) => entry.receivedSeq);
183
+ const backgroundSeqs = value.queue.slice(urgentCount).map((entry) => entry.receivedSeq);
184
+ const isAscending = (seqs) => seqs.every((seq, index) => index === 0 || seq > seqs[index - 1]);
185
+ if (!isAscending(urgentSeqs) || !isAscending(backgroundSeqs))
186
+ throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
164
187
  if (value.nextSeq <= maxReceivedSeq)
165
188
  throw new Error('INVALID_FACT_INBOX_SNAPSHOT');
166
189
  for (const [eventId, receivedSeq] of seen)
167
190
  inbox.seen.set(eventId, receivedSeq);
168
191
  inbox.nextSeq = value.nextSeq;
192
+ inbox.urgentCount = urgentCount;
169
193
  return inbox;
170
194
  }
171
- clear() { this.queue.length = 0; }
195
+ clear() { this.queue.length = 0; this.urgentCount = 0; }
172
196
  }
173
197
  export class ObservationInbox {
174
198
  maxEntries;
@@ -1,4 +1,4 @@
1
- import type { RuntimeEventInput, RuntimeState, RuntimeError, ContextVersion, JsonValue, AgentRecord, LaneRecord, EffectRecord, WaitRecord, ResultRecord, FindingRecord, ArtifactRecord, LaneId, WaitId, EffectId, HistoryRecord, MergeProposal, ToolCallCorrelation, PrivacyMetadata } from './types.js';
1
+ import type { RuntimeEventInput, RuntimeState, RuntimeError, ContextVersion, JsonValue, AgentRecord, LaneRecord, EffectRecord, WaitRecord, ResultRecord, FindingRecord, ArtifactRecord, LaneId, WaitId, EffectId, HistoryRecord, MergeProposal, ToolCallCorrelation, PrivacyMetadata, HumanInputRecord } from './types.js';
2
2
  export type Mutation = {
3
3
  op: 'setAgent';
4
4
  agentId: string;
@@ -36,6 +36,10 @@ export type Mutation = {
36
36
  } | {
37
37
  op: 'setToolCallCorrelation';
38
38
  record: ToolCallCorrelation;
39
+ } | {
40
+ op: 'setHumanInput';
41
+ inputId: string;
42
+ record: HumanInputRecord;
39
43
  } | {
40
44
  op: 'insertMergeProposal';
41
45
  proposal: MergeProposal;
@@ -21,7 +21,7 @@ export function forkRuntimeStateForAdmission(state, mutations) {
21
21
  for (const id of dirtyLanes) {
22
22
  const lane = lanes.get(id);
23
23
  if (lane)
24
- lanes.set(id, { ...lane, ...(lane.visibleResultRefs === undefined ? {} : { visibleResultRefs: new Set(lane.visibleResultRefs) }) });
24
+ lanes.set(id, { ...lane, ...(lane.visibleResultRefs === undefined ? {} : { visibleResultRefs: new Set(lane.visibleResultRefs) }), ...(lane.pendingHumanInputs === undefined ? {} : { pendingHumanInputs: structuredClone(lane.pendingHumanInputs) }) });
25
25
  }
26
26
  return {
27
27
  ...state,
@@ -33,6 +33,7 @@ export function forkRuntimeStateForAdmission(state, mutations) {
33
33
  artifacts: new Map(state.artifacts),
34
34
  toolCallCorrelations: new Map(state.toolCallCorrelations),
35
35
  mergeProposals: new Map(state.mergeProposals),
36
+ humanInputs: new Map(state.humanInputs),
36
37
  events: state.events.slice(),
37
38
  nextIds: { ...state.nextIds },
38
39
  trustedSanitizerIds: new Set(state.trustedSanitizerIds),
@@ -91,6 +92,9 @@ export function apply(state, mutations, defaults = {}) {
91
92
  case 'setToolCallCorrelation':
92
93
  state.toolCallCorrelations.set(mutation.record.toolCallId, mutation.record);
93
94
  break;
95
+ case 'setHumanInput':
96
+ state.humanInputs.set(mutation.inputId, mutation.record);
97
+ break;
94
98
  case 'insertMergeProposal':
95
99
  state.mergeProposals.set(mutation.proposal.id, mutation.proposal);
96
100
  break;
@@ -123,6 +123,8 @@ export interface LaneRecord {
123
123
  resume: ResumePoint;
124
124
  series?: SeriesLaneSpec;
125
125
  pendingResumeInput?: ResumeInput;
126
+ /** Human messages steered into this lane while it was executing or waiting. */
127
+ pendingHumanInputs?: HumanInputRecord[];
126
128
  /** Control proposals that arrived while another ResumeInput occupied the slot. */
127
129
  pendingControlProposals?: ControlProposal[];
128
130
  contextSnapshotVersion: ContextVersion;
@@ -319,6 +321,9 @@ export type ResumeInput = {
319
321
  } | {
320
322
  type: 'submitted';
321
323
  targets: Record<string, TargetRef>;
324
+ } | {
325
+ type: 'human';
326
+ input: HumanInputRecord;
322
327
  } | {
323
328
  type: 'control_error';
324
329
  error: RuntimeError;
@@ -499,14 +504,19 @@ export interface LLMContextSpec {
499
504
  eventIds: string[];
500
505
  toolSetId: string;
501
506
  instruction: string;
507
+ conversation?: ConversationMessage[];
502
508
  privacy: PrivacyLabel;
503
509
  privacyRefs: ProvenanceRef[];
504
510
  privacyTaints?: PrivacyTaint[];
505
511
  }
512
+ export interface ConversationMessage {
513
+ role: 'system' | 'user' | 'assistant';
514
+ content: string;
515
+ }
506
516
  export interface LLMRequestProjection {
507
517
  contextSpec: LLMContextSpec;
508
518
  blocks: Array<{
509
- kind: 'system' | 'policy' | 'tools' | 'global' | 'history' | 'lane' | 'events' | 'results' | 'artifacts' | 'instruction';
519
+ kind: 'system' | 'policy' | 'tools' | 'global' | 'conversation' | 'history' | 'lane' | 'events' | 'results' | 'artifacts' | 'instruction';
510
520
  content: JsonValue;
511
521
  }>;
512
522
  prefixHash: string;
@@ -559,6 +569,7 @@ export interface RuntimeState {
559
569
  artifacts: Map<ArtifactRef, ArtifactRecord>;
560
570
  toolCallCorrelations: Map<string, ToolCallCorrelation>;
561
571
  mergeProposals: Map<string, MergeProposal>;
572
+ humanInputs: Map<string, HumanInputRecord>;
562
573
  events: RuntimeEvent[];
563
574
  eventsCompactedThrough?: number;
564
575
  nextIds: {
@@ -598,6 +609,20 @@ export interface ToolCallCorrelation {
598
609
  toolEffectId: EffectId;
599
610
  resultRef?: ResultRef;
600
611
  }
612
+ /** Durable external input accepted while other Effects are still running. */
613
+ export interface HumanInputRecord {
614
+ id: string;
615
+ agentId: AgentId;
616
+ value: JsonValue;
617
+ receivedAt: number;
618
+ status: 'pending' | 'consumed' | 'deferred';
619
+ targetEffectId?: EffectId;
620
+ handledByLaneId?: LaneId;
621
+ decision?: 'respond' | 'steer' | 'spawn' | 'defer' | 'cancel';
622
+ decisionReason?: string;
623
+ decisionModelId?: string;
624
+ decidedAt?: number;
625
+ }
601
626
  export declare function createRuntimeState(maxTotalLanes?: number, options?: {
602
627
  maxQueuedEffects?: number;
603
628
  maxRunning?: Partial<Record<ConcurrencyClass, number>>;
@@ -84,7 +84,7 @@ export function privacyTaintsForDerivedRefs(state, lane, refs) {
84
84
  return output;
85
85
  }
86
86
  export function createRuntimeState(maxTotalLanes = 64, options = {}) {
87
- return { now: 0, agents: new Map(), lanes: new Map(), effects: new Map(), waits: new Map(), results: new Map(), artifacts: new Map(), toolCallCorrelations: new Map(), mergeProposals: new Map(), events: [], nextIds: { agent: 1, lane: 1, effect: 1, wait: 1, result: 1, artifact: 1, proposal: 1, event: 1 }, maxTotalLanes, maxQueuedEffects: options.maxQueuedEffects ?? 256, maxRunning: { llm: 4, tool: 16, agent: 4, none: Number.POSITIVE_INFINITY, ...(options.maxRunning ?? {}) }, forkAffinity: options.forkAffinity ?? 'advise', historySoftTokens: options.historySoftTokens ?? 8_000, historyHardTokens: options.historyHardTokens ?? 16_000, maxResultSummaryBytes: options.maxResultSummaryBytes ?? 4_096, trustedSanitizerIds: new Set(options.trustedSanitizerIds ?? []) };
87
+ return { now: 0, agents: new Map(), lanes: new Map(), effects: new Map(), waits: new Map(), results: new Map(), artifacts: new Map(), toolCallCorrelations: new Map(), mergeProposals: new Map(), humanInputs: new Map(), events: [], nextIds: { agent: 1, lane: 1, effect: 1, wait: 1, result: 1, artifact: 1, proposal: 1, event: 1 }, maxTotalLanes, maxQueuedEffects: options.maxQueuedEffects ?? 256, maxRunning: { llm: 4, tool: 16, agent: 4, none: Number.POSITIVE_INFINITY, ...(options.maxRunning ?? {}) }, forkAffinity: options.forkAffinity ?? 'advise', historySoftTokens: options.historySoftTokens ?? 8_000, historyHardTokens: options.historyHardTokens ?? 16_000, maxResultSummaryBytes: options.maxResultSummaryBytes ?? 4_096, trustedSanitizerIds: new Set(options.trustedSanitizerIds ?? []) };
88
88
  }
89
89
  export function privacyRank(label) { return label === 'public' ? 0 : label === 'cloud_allowed' ? 1 : 2; }
90
90
  export function strictestPrivacy(labels) { return labels.reduce((current, next) => privacyRank(next) > privacyRank(current) ? next : current, 'public'); }
@@ -1,6 +1,6 @@
1
1
  import { z, type ZodTypeAny } from 'zod';
2
2
  import type { LaneProgram } from '../scheduler/runtime.js';
3
- import type { ContextDelta, JsonValue, LaneRecord, ResultRef, ProvenanceRef, RuntimeAction, ResumeInput, ProgressWatchdogState, ContextOp, LaneId, PrivacyLabel, RuntimeError, MergeProposal, ResourceLockSpec, Outcome, WaitResolution } from '../core/types.js';
3
+ import type { ContextDelta, ConversationMessage, JsonValue, LaneRecord, ResultRef, ProvenanceRef, RuntimeAction, ResumeInput, ProgressWatchdogState, ContextOp, LaneId, PrivacyLabel, RuntimeError, MergeProposal, ResourceLockSpec, Outcome, WaitResolution, HumanInputRecord } from '../core/types.js';
4
4
  import type { ProgramRef } from './templates.js';
5
5
  import type { RuntimeToolDiscoveryQuery } from '../tools/registry.js';
6
6
  export type NextStepTarget<TState = unknown> = string | {
@@ -33,11 +33,13 @@ export interface StepInputs {
33
33
  findings?: ResultRef[];
34
34
  artifacts?: string[];
35
35
  events?: string[];
36
+ conversation?: ConversationMessage[];
36
37
  toolDiscovery?: RuntimeToolDiscoveryQuery;
37
38
  }
38
39
  export interface HistoryCompactionOptions {
39
40
  summarizeTask: string;
40
41
  keepRecentRounds: number;
42
+ instruction?: string;
41
43
  }
42
44
  export interface HistoryRecordMeta {
43
45
  seq: number;
@@ -76,6 +78,7 @@ export interface StepContext<TState = JsonValue> {
76
78
  now: number;
77
79
  watchdog?: ProgressWatchdogState;
78
80
  resumeInput?: ResumeInput;
81
+ humanInputs?: readonly HumanInputRecord[];
79
82
  results: {
80
83
  meta(ref: ResultRef): ResultMeta | undefined;
81
84
  summary(ref: ResultRef): JsonValue | undefined;