@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.
@@ -85,6 +85,8 @@ export declare class WorkerCoordinator implements WorkerCoordinatorContract {
85
85
  private readonly persistenceBackend;
86
86
  private persistenceDigest;
87
87
  private persistencePending;
88
+ private persistenceDirty;
89
+ private persistenceScheduled;
88
90
  private sequence;
89
91
  constructor(options?: WorkerCoordinatorOptions);
90
92
  static restore(snapshot: WorkerCoordinatorSnapshot, options?: WorkerCoordinatorOptions): WorkerCoordinator;
@@ -173,6 +173,8 @@ export class WorkerCoordinator {
173
173
  persistenceBackend;
174
174
  persistenceDigest;
175
175
  persistencePending = Promise.resolve();
176
+ persistenceDirty = false;
177
+ persistenceScheduled = false;
176
178
  sequence = 1;
177
179
  constructor(options = {}) { this.persistenceBackend = options.persistenceBackend; }
178
180
  static restore(snapshot, options = {}) {
@@ -349,10 +351,28 @@ export class WorkerCoordinator {
349
351
  schedulePersistence() {
350
352
  if (!this.persistenceBackend)
351
353
  return;
354
+ this.persistenceDirty = true;
355
+ if (this.persistenceScheduled)
356
+ return;
357
+ this.persistenceScheduled = true;
352
358
  const operation = this.persistencePending.catch(() => undefined).then(async () => {
353
- const snapshot = this.snapshot();
354
- await this.persistenceBackend.save(snapshot, this.persistenceDigest);
355
- this.persistenceDigest = snapshot.integrity?.digest;
359
+ while (this.persistenceDirty) {
360
+ this.persistenceDirty = false;
361
+ const snapshot = this.snapshot();
362
+ const digest = snapshot.integrity?.digest;
363
+ if (digest === this.persistenceDigest)
364
+ continue;
365
+ try {
366
+ await this.persistenceBackend.save(snapshot, this.persistenceDigest);
367
+ this.persistenceDigest = digest;
368
+ }
369
+ catch (cause) {
370
+ this.persistenceDirty = true;
371
+ throw cause;
372
+ }
373
+ }
374
+ }).finally(() => {
375
+ this.persistenceScheduled = false;
356
376
  });
357
377
  this.persistencePending = operation;
358
378
  }
@@ -668,7 +688,7 @@ export class SqliteDistributedWorkerCoordinator {
668
688
  result.reject(task.error ?? { code: 'WORKER_FAILED', message: 'WORKER_FAILED' });
669
689
  else
670
690
  result.reject(new Error('WORKER_CANCELLED'));
671
- }, 10);
691
+ }, 50);
672
692
  watcher.unref();
673
693
  this.watchers.set(taskId, watcher);
674
694
  }
@@ -1,4 +1,4 @@
1
- import type { AgentRecord, ArtifactRecord, EffectRecord, JsonValue, LaneRecord, MergeProposal, PrivacyLabel, PrivacyMetadata, ResultRecord, RuntimeEvent, RuntimeState, WaitRecord, ToolCallCorrelation } from '../core/types.js';
1
+ import type { AgentRecord, ArtifactRecord, EffectRecord, JsonValue, LaneRecord, MergeProposal, PrivacyLabel, PrivacyMetadata, ResultRecord, RuntimeEvent, RuntimeState, WaitRecord, ToolCallCorrelation, HumanInputRecord } from '../core/types.js';
2
2
  export interface SessionSnapshot {
3
3
  schemaVersion: 1;
4
4
  state: {
@@ -18,6 +18,7 @@ export interface SessionSnapshot {
18
18
  artifacts?: Array<[string, ArtifactRecord]>;
19
19
  toolCallCorrelations?: Array<[string, ToolCallCorrelation]>;
20
20
  mergeProposals: Array<[string, MergeProposal]>;
21
+ humanInputs?: Array<[string, HumanInputRecord]>;
21
22
  events: RuntimeEvent[];
22
23
  eventsCompactedThrough?: number;
23
24
  nextIds: RuntimeState['nextIds'];
@@ -304,6 +304,17 @@ function validateStateConfiguration(value) {
304
304
  throw new Error('INVALID_SESSION_SNAPSHOT');
305
305
  if (value.trustedSanitizerIds !== undefined && (!Array.isArray(value.trustedSanitizerIds) || new Set(value.trustedSanitizerIds).size !== value.trustedSanitizerIds.length || value.trustedSanitizerIds.some((id) => typeof id !== 'string' || id.length === 0)))
306
306
  throw new Error('INVALID_SESSION_SNAPSHOT');
307
+ if (value.humanInputs !== undefined) {
308
+ const ids = new Set();
309
+ for (const entry of value.humanInputs) {
310
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || entry[0].length === 0 || ids.has(entry[0]))
311
+ throw new Error('INVALID_SESSION_SNAPSHOT');
312
+ const input = entry[1];
313
+ if (!input || input.id !== entry[0] || typeof input.agentId !== 'string' || input.agentId.length === 0 || !['pending', 'consumed', 'deferred'].includes(input.status) || typeof input.receivedAt !== 'number' || !Number.isFinite(input.receivedAt))
314
+ throw new Error('INVALID_SESSION_SNAPSHOT');
315
+ ids.add(entry[0]);
316
+ }
317
+ }
307
318
  }
308
319
  export function exportRuntimeState(state) {
309
320
  return {
@@ -318,6 +329,7 @@ export function exportRuntimeState(state) {
318
329
  artifacts: [...state.artifacts.entries()].map(([ref, artifact]) => [ref, structuredClone(artifact)]),
319
330
  toolCallCorrelations: [...state.toolCallCorrelations.entries()].map(([id, correlation]) => [id, structuredClone(correlation)]),
320
331
  mergeProposals: [...state.mergeProposals.entries()].map(([id, proposal]) => [id, structuredClone(proposal)]),
332
+ humanInputs: [...state.humanInputs.entries()].map(([id, input]) => [id, structuredClone(input)]),
321
333
  events: state.events.map((event) => normalizeRuntimeEvent(event, event.seq, { sessionId: event.sessionId, timestamp: event.timestamp })),
322
334
  ...(state.eventsCompactedThrough === undefined ? {} : { eventsCompactedThrough: state.eventsCompactedThrough }),
323
335
  nextIds: { ...state.nextIds },
@@ -440,6 +452,8 @@ export function importRuntimeState(snapshot) {
440
452
  state.toolCallCorrelations.set(id, structuredClone(correlation));
441
453
  for (const [id, proposal] of value.state.mergeProposals ?? [])
442
454
  state.mergeProposals.set(id, structuredClone(proposal));
455
+ for (const [id, input] of value.state.humanInputs ?? [])
456
+ state.humanInputs.set(id, structuredClone(input));
443
457
  state.events = value.state.events.map((event) => normalizeRuntimeEvent(event, event.seq, { sessionId: event.sessionId, timestamp: event.timestamp }));
444
458
  if (value.state.eventsCompactedThrough !== undefined)
445
459
  state.eventsCompactedThrough = value.state.eventsCompactedThrough;
@@ -7,7 +7,7 @@ import { ContextBuilder, contentHash, estimateHistoryTokens, hasUnsafePathSegmen
7
7
  const isLocal = (value) => 'local' in value;
8
8
  const clone = (value) => structuredClone(value);
9
9
  const resultMetadata = (value) => ({ sizeBytes: Buffer.byteLength(stableSerialize(value), 'utf8'), contentHash: contentHash(value) });
10
- const laneCopy = (lane) => ({ ...lane, resume: clone(lane.resume), context: clone(lane.context), ...(lane.visibleResultRefs === undefined ? {} : { visibleResultRefs: new Set(lane.visibleResultRefs) }), children: new Set(lane.children), ownedEffectIds: new Set(lane.ownedEffectIds), ...(lane.pendingResumeInput === undefined ? {} : { pendingResumeInput: clone(lane.pendingResumeInput) }), ...(lane.pendingControlProposals === undefined ? {} : { pendingControlProposals: clone(lane.pendingControlProposals) }), ...(lane.pendingOutcome === undefined ? {} : { pendingOutcome: clone(lane.pendingOutcome) }) });
10
+ const laneCopy = (lane) => ({ ...lane, resume: clone(lane.resume), context: clone(lane.context), ...(lane.visibleResultRefs === undefined ? {} : { visibleResultRefs: new Set(lane.visibleResultRefs) }), children: new Set(lane.children), ownedEffectIds: new Set(lane.ownedEffectIds), ...(lane.pendingResumeInput === undefined ? {} : { pendingResumeInput: clone(lane.pendingResumeInput) }), ...(lane.pendingHumanInputs === undefined ? {} : { pendingHumanInputs: clone(lane.pendingHumanInputs) }), ...(lane.pendingControlProposals === undefined ? {} : { pendingControlProposals: clone(lane.pendingControlProposals) }), ...(lane.pendingOutcome === undefined ? {} : { pendingOutcome: clone(lane.pendingOutcome) }) });
11
11
  function isRuntimeJsonValue(value, seen = new Set()) {
12
12
  if (value === null || typeof value === 'string' || typeof value === 'boolean')
13
13
  return true;
@@ -614,7 +614,12 @@ function prepareLLMInput(state, lane, submission) {
614
614
  const rejectedInputs = readRefs('rejectedOutputRefs');
615
615
  const artifactInputs = readRefs('artifacts');
616
616
  const eventInputs = readRefs('events');
617
- const inputError = resultInputs.error ?? findingInputs.error ?? rejectedInputs.error ?? artifactInputs.error ?? eventInputs.error;
617
+ const rawConversation = rawInputs.conversation;
618
+ const conversation = rawConversation === undefined ? [] : Array.isArray(rawConversation) && rawConversation.every((item) => item && typeof item === 'object' && !Array.isArray(item) && item.role !== undefined && ['system', 'user', 'assistant'].includes(String(item.role)) && typeof item.content === 'string')
619
+ ? rawConversation
620
+ : undefined;
621
+ const conversationError = rawConversation !== undefined && conversation === undefined ? 'INVALID_LLM_CONVERSATION' : undefined;
622
+ const inputError = resultInputs.error ?? findingInputs.error ?? rejectedInputs.error ?? artifactInputs.error ?? eventInputs.error ?? conversationError;
618
623
  if (inputError)
619
624
  return { error: inputError };
620
625
  const resultRefs = [...new Set([...(resultInputs.refs ?? []), ...(findingInputs.refs ?? []), ...(rejectedInputs.refs ?? [])])];
@@ -623,7 +628,7 @@ function prepareLLMInput(state, lane, submission) {
623
628
  const agent = state.agents.get(lane.agentId);
624
629
  if (!agent)
625
630
  return { error: 'UNKNOWN_AGENT' };
626
- const projection = new ContextBuilder(state).build({ agent, lane, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: eventInputs.refs ?? [], instruction, ...(typeof input.system === 'string' ? { system: input.system } : {}), ...(input.policy === undefined ? {} : { policy: input.policy }), ...(input.tools === undefined ? {} : { tools: input.tools }), toolSetId: typeof input.toolSetId === 'string' ? input.toolSetId : 'default' });
631
+ const projection = new ContextBuilder(state).build({ agent, lane, resultRefs, ...(artifactRefs.length ? { artifactRefs } : {}), eventIds: eventInputs.refs ?? [], ...(conversation === undefined || conversation.length === 0 ? {} : { conversation }), instruction, ...(typeof input.system === 'string' ? { system: input.system } : {}), ...(input.policy === undefined ? {} : { policy: input.policy }), ...(input.tools === undefined ? {} : { tools: input.tools }), toolSetId: typeof input.toolSetId === 'string' ? input.toolSetId : 'default' });
627
632
  return { input: { ...input, request: projection } };
628
633
  }
629
634
  catch (cause) {
@@ -708,8 +713,13 @@ export function validateStep(state, laneId, output) {
708
713
  else if (output.adoptCommittedContext)
709
714
  return { rejection: error('INVALID_ADOPT_COMMITTED_CONTEXT', 'adoptCommittedContext requires a ContextDelta') };
710
715
  const compactRequested = output.contextDelta?.target === 'lane' && output.contextDelta.ops.some((op) => op.op === 'compact_history');
716
+ // ReAct can discover pressure only after consuming a model result. Permit
717
+ // the two-step compaction handoff and its summary request to cross the hard
718
+ // threshold; the following compact_history delta removes the old records.
719
+ const compactionHandoff = output.next.step === '$compact:summarize' && actions.length === 0;
720
+ const compactionSummarySubmission = actions.some((action) => action.type === 'submit_effects' && action.effects.some((effect) => effect.key === '$compact-summary'));
711
721
  const nextHistoryTokens = estimateHistoryTokens(workingLane.context.history);
712
- if (nextHistoryTokens > state.historyHardTokens && !compactRequested)
722
+ if (nextHistoryTokens > state.historyHardTokens && !compactRequested && !compactionHandoff && !compactionSummarySubmission)
713
723
  return { rejection: error('CONTEXT_TOO_LARGE', 'Lane history exceeded hardTokens and must be compacted before another Step can commit.', { historyTokens: nextHistoryTokens, softTokens: state.historySoftTokens, hardTokens: state.historyHardTokens }) };
714
724
  const pressure = historyPressure(workingLane.context.history, state.historySoftTokens, state.historyHardTokens);
715
725
  if (pressure)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-runtime",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zhuhengtan/Pulse"