@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 +3 -0
- package/dist/index.js +72 -10
- package/package.json +4 -4
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 () => {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
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.
|
|
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.
|
|
20
|
-
"@hunterzhu/pulse-runtime": "0.1.
|
|
21
|
-
"@hunterzhu/pulse-tool-sdk": "0.1.
|
|
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
|
}
|