@hunterzhu/pulse-server 0.1.5 → 0.1.6

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/index.d.ts CHANGED
@@ -61,6 +61,8 @@ export interface RunHandle {
61
61
  }>;
62
62
  cancel(reason?: string): Promise<void>;
63
63
  reply(effectId: string, value: JsonValue): Promise<void>;
64
+ /** Submit a human message while this run is active. */
65
+ submitHumanInput(text: string, targetEffectId?: string): Promise<void>;
64
66
  }
65
67
  export interface ConversationHandle {
66
68
  readonly id: string;
@@ -117,6 +119,7 @@ export declare class LocalHost {
117
119
  sendMessage(conversationId: string, input: UserMessageInput): Promise<RunHandle>;
118
120
  resumeRun(conversationId: string): Promise<RunHandle>;
119
121
  private resultText;
122
+ private interactionResultTexts;
120
123
  private toolSettlementObservation;
121
124
  private projectEvents;
122
125
  close(): Promise<void>;
package/dist/index.js CHANGED
@@ -431,11 +431,34 @@ export class LocalHost {
431
431
  }
432
432
  makeRunHandle(conversationId, runId, runtime, session) {
433
433
  let finalized;
434
- const finish = () => finalized ??= (async () => { const outcome = await session.outcome(); const text = this.resultText(runtime, outcome.resultRef); await this.appendMessage(conversationId, { id: `msg-${randomUUID()}`, role: 'assistant', text: text ?? '', runId, createdAt: new Date().toISOString() }); const current = await this.readManifest(conversationId); const artifacts = [...runtime.state.results.values()].flatMap((result) => { const value = result.value; if (!value || typeof value !== 'object' || Array.isArray(value))
435
- return []; const record = value; if (typeof record.path !== 'string' || typeof record.hash !== 'string' || typeof record.bytes !== 'number')
436
- return []; return [{ path: record.path, hash: record.hash, bytes: record.bytes, ...(typeof record.mediaType === 'string' ? { mediaType: record.mediaType } : {}), ...(typeof record.label === 'string' ? { label: record.label } : {}), runId }]; }); current.artifacts = [...(current.artifacts ?? []).filter((item) => item.runId !== runId), ...artifacts]; if (current.activeRunId === runId)
437
- delete current.activeRunId; current.updatedAt = new Date().toISOString(); await writeFile(this.manifestPath(conversationId), JSON.stringify(current, null, 2)); this.active.delete(runId); this.approvedToolCalls.delete(runId); await runtime.flushPersistence(); await writeFile(join(this.runDir(conversationId, runId), 'outcome.json'), JSON.stringify({ schemaVersion: 1, ...outcome, ...(text === undefined ? {} : { text }), completedAt: new Date().toISOString() }, null, 2)); return { ...outcome, ...(text === undefined ? {} : { text }) }; })().finally(async () => { await this.releaseConversationLock(conversationId); });
438
- const events = this.projectEvents(conversationId, runId, session, finish);
434
+ const finish = () => finalized ??= (async () => {
435
+ const outcome = await session.outcome();
436
+ const text = this.resultText(runtime, outcome.resultRef);
437
+ // A free-form human input runs as a detached child Agent. Its answer is
438
+ // part of the same user-visible run, but it is not the root Outcome.
439
+ // Persist each child answer before the root answer so a restored
440
+ // conversation has the same order the user saw in the event stream.
441
+ for (const child of this.interactionResultTexts(runtime, session.agentId)) {
442
+ await this.appendMessage(conversationId, { id: `msg-${randomUUID()}`, role: 'assistant', text: child.text, runId, createdAt: new Date().toISOString() });
443
+ }
444
+ if (text !== undefined)
445
+ await this.appendMessage(conversationId, { id: `msg-${randomUUID()}`, role: 'assistant', text, runId, createdAt: new Date().toISOString() });
446
+ const current = await this.readManifest(conversationId);
447
+ const artifacts = [...runtime.state.results.values()].flatMap((result) => { const value = result.value; if (!value || typeof value !== 'object' || Array.isArray(value))
448
+ return []; const record = value; if (typeof record.path !== 'string' || typeof record.hash !== 'string' || typeof record.bytes !== 'number')
449
+ return []; return [{ path: record.path, hash: record.hash, bytes: record.bytes, ...(typeof record.mediaType === 'string' ? { mediaType: record.mediaType } : {}), ...(typeof record.label === 'string' ? { label: record.label } : {}), runId }]; });
450
+ current.artifacts = [...(current.artifacts ?? []).filter((item) => item.runId !== runId), ...artifacts];
451
+ if (current.activeRunId === runId)
452
+ delete current.activeRunId;
453
+ current.updatedAt = new Date().toISOString();
454
+ await writeFile(this.manifestPath(conversationId), JSON.stringify(current, null, 2));
455
+ this.active.delete(runId);
456
+ this.approvedToolCalls.delete(runId);
457
+ await runtime.flushPersistence();
458
+ await writeFile(join(this.runDir(conversationId, runId), 'outcome.json'), JSON.stringify({ schemaVersion: 1, ...outcome, ...(text === undefined ? {} : { text }), completedAt: new Date().toISOString() }, null, 2));
459
+ return { ...outcome, ...(text === undefined ? {} : { text }) };
460
+ })().finally(async () => { await this.releaseConversationLock(conversationId); });
461
+ const events = this.projectEvents(conversationId, runId, runtime, session, finish);
439
462
  return { id: runId, conversationId, events, outcome: finish, cancel: async (reason = 'USER_REQUESTED') => { await session.cancel(reason); }, reply: async (effectId, value) => { const effect = runtime.state.effects.get(effectId); const approved = value && typeof value === 'object' && !Array.isArray(value) && value.approved === true; if (approved && effect?.kind === 'human' && effect.input && typeof effect.input === 'object' && !Array.isArray(effect.input)) {
440
463
  const calls = effect.input.tools;
441
464
  if (Array.isArray(calls)) {
@@ -445,7 +468,8 @@ export class LocalHost {
445
468
  if (call && typeof call === 'object' && !Array.isArray(call) && typeof call.toolCallId === 'string')
446
469
  approvedIds.add(call.toolCallId);
447
470
  }
448
- } await session.reply(effectId, value); } };
471
+ } await session.reply(effectId, value); }, submitHumanInput: async (text, targetEffectId) => { if (!text.trim())
472
+ throw new Error('MESSAGE_REQUIRED'); const inputId = `human-${randomUUID()}`; await this.appendMessage(conversationId, { id: `msg-${randomUUID()}`, role: 'user', text, runId, createdAt: new Date().toISOString() }); await session.submitHumanInput(inputId, { text }, targetEffectId); } };
449
473
  }
450
474
  async sendMessage(conversationId, input) {
451
475
  if (!input.text.trim())
@@ -474,6 +498,7 @@ export class LocalHost {
474
498
  const { runtime, registry } = this.runtimeFor(conversationId, runId, manifest.cwd);
475
499
  const program = buildProgram(registry.list().map((tool) => tool.name), manifest.cwd)(this.options.approvalMode ?? 'ask');
476
500
  runtime.register(program);
501
+ runtime.setHumanInputProgram(program);
477
502
  const { agentId } = runtime.createAgent({ goal, program });
478
503
  const session = runtime.start(agentId);
479
504
  this.active.set(runId, { runtime, session, conversationId, runId });
@@ -498,8 +523,10 @@ export class LocalHost {
498
523
  return this.makeRunHandle(conversationId, runId, existing.runtime, existing.session);
499
524
  await this.acquireConversationLock(conversationId, runId);
500
525
  try {
501
- const { runtime } = await this.restoreRuntimeFor(conversationId, runId, manifest.cwd);
502
- const agent = [...runtime.state.agents.values()][0];
526
+ const { runtime, registry } = await this.restoreRuntimeFor(conversationId, runId, manifest.cwd);
527
+ const interactionProgram = buildProgram(registry.list().map((tool) => tool.name), manifest.cwd)(this.options.approvalMode ?? 'ask');
528
+ runtime.setHumanInputProgram(interactionProgram);
529
+ const agent = [...runtime.state.agents.values()].find((candidate) => candidate.parentAgentId === undefined);
503
530
  if (!agent)
504
531
  throw new Error('RESTORED_AGENT_NOT_FOUND');
505
532
  const session = runtime.start(agent.id);
@@ -525,6 +552,22 @@ export class LocalHost {
525
552
  return JSON.stringify(first); const firstRecord = first; const textRef = firstRecord.textRef; const value = typeof textRef === 'string' ? runtime.state.results.get(textRef)?.value : first; if (typeof value === 'string')
526
553
  return value; if (value && typeof value === 'object' && !Array.isArray(value) && typeof value.text === 'string')
527
554
  return value.text; return value === undefined ? undefined : JSON.stringify(value, null, 2); }
555
+ interactionResultTexts(runtime, rootAgentId) {
556
+ const descendants = new Set();
557
+ const visit = (parentId) => {
558
+ for (const agent of runtime.state.agents.values()) {
559
+ if (agent.parentAgentId !== parentId || descendants.has(agent.id))
560
+ continue;
561
+ descendants.add(agent.id);
562
+ visit(agent.id);
563
+ }
564
+ };
565
+ visit(rootAgentId);
566
+ return [...runtime.state.agents.values()]
567
+ .filter((agent) => descendants.has(agent.id))
568
+ .map((agent) => ({ agentId: agent.id, text: this.resultText(runtime, runtime.state.lanes.get(agent.rootLaneId)?.resultRef) }))
569
+ .filter((item) => item.text !== undefined && item.text.length > 0);
570
+ }
528
571
  toolSettlementObservation(runId, effectId, data) {
529
572
  const effect = this.active.get(runId)?.runtime.state.effects.get(effectId);
530
573
  if (effect?.kind !== 'tool')
@@ -537,14 +580,18 @@ export class LocalHost {
537
580
  const args = input.arguments && typeof input.arguments === 'object' && !Array.isArray(input.arguments) ? input.arguments : {};
538
581
  return { tool: input.name, toolCallId: effect.toolCallId ?? effectId, args, status, ...(outcome.error === undefined ? {} : { result: outcome.error }) };
539
582
  }
540
- async *projectEvents(conversationId, runId, session, finish) {
583
+ async *projectEvents(conversationId, runId, runtime, session, finish) {
541
584
  let seq = 0;
585
+ const textAgents = new Set();
542
586
  for await (const event of session.stream()) {
543
587
  seq++;
544
588
  if (event.kind === 'observation') {
545
589
  const observation = event.observation;
546
- if (observation.type === 'chunk')
590
+ if (observation.type === 'chunk') {
591
+ if (typeof observation.agentId === 'string')
592
+ textAgents.add(observation.agentId);
547
593
  yield { schemaVersion: 1, type: 'text', conversationId, runId, seq, data: observation.data ?? '' };
594
+ }
548
595
  else
549
596
  yield { schemaVersion: 1, type: 'observation', conversationId, runId, seq, data: event.observation ?? null };
550
597
  continue;
@@ -571,6 +618,21 @@ export class LocalHost {
571
618
  }
572
619
  try {
573
620
  const outcome = await finish();
621
+ // Some adapters only return a final LLM message and do not stream
622
+ // observations. Project that result here so CLI/web clients still get a
623
+ // visible answer. Child interaction Agents use the same fallback and
624
+ // are emitted before the root answer in creation order.
625
+ const agentTexts = [
626
+ ...this.interactionResultTexts(runtime, session.agentId),
627
+ ...(outcome.text === undefined ? [] : [{ agentId: session.agentId, text: outcome.text }]),
628
+ ];
629
+ for (const item of agentTexts) {
630
+ if (textAgents.has(item.agentId) || item.text.length === 0)
631
+ continue;
632
+ seq++;
633
+ textAgents.add(item.agentId);
634
+ yield { schemaVersion: 1, type: 'text', conversationId, runId, seq, data: item.text };
635
+ }
574
636
  yield { schemaVersion: 1, type: 'complete', conversationId, runId, seq: seq + 1, data: { status: outcome.status } };
575
637
  }
576
638
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zhuhengtan/Pulse"
@@ -16,9 +16,9 @@
16
16
  "registry": "https://registry.npmjs.org"
17
17
  },
18
18
  "dependencies": {
19
- "@hunterzhu/pulse-adapters": "0.1.5",
20
- "@hunterzhu/pulse-runtime": "0.1.5",
21
- "@hunterzhu/pulse-tool-sdk": "0.1.5",
19
+ "@hunterzhu/pulse-adapters": "0.1.6",
20
+ "@hunterzhu/pulse-runtime": "0.1.6",
21
+ "@hunterzhu/pulse-tool-sdk": "0.1.6",
22
22
  "zod": "^3.24.1"
23
23
  }
24
24
  }