@hunterzhu/pulse-runtime 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @hunterzhu/pulse-runtime
2
2
 
3
+ [简体中文](https://github.com/zhuhengtan/Pulse/blob/main/packages/runtime/README.zh-CN.md)
4
+
3
5
  The core agent runtime for Pulse: programs, lanes, agents, and outcomes.
4
6
 
5
7
  ## Requirements
@@ -40,3 +42,7 @@ tools is described in the docs.
40
42
 
41
43
  - [Documentation](https://github.com/zhuhengtan/Pulse#readme)
42
44
  - [Source](https://github.com/zhuhengtan/Pulse/tree/main/packages/runtime)
45
+
46
+ ## License
47
+
48
+ This package is licensed under [PolyForm Noncommercial 1.0.0](./LICENSE). Commercial use requires separate permission. This is a source-available noncommercial license, not an OSI-approved open-source license.
@@ -9,6 +9,7 @@ export type NextStepTarget<TState = unknown> = string | {
9
9
  complete: {
10
10
  value?: JsonValue;
11
11
  privacy?: PrivacyLabel;
12
+ derivedFrom?: ProvenanceRef[];
12
13
  children?: 'reject_if_active' | 'cancel' | 'await';
13
14
  };
14
15
  } | {
@@ -89,6 +90,7 @@ export interface StepContext<TState = JsonValue> {
89
90
  results: {
90
91
  meta(ref: ResultRef): ResultMeta | undefined;
91
92
  summary(ref: ResultRef): JsonValue | undefined;
93
+ read(ref: ResultRef): JsonValue | undefined;
92
94
  };
93
95
  mergeProposals: ReadonlyArray<MergeProposal>;
94
96
  mutateLane(mutator: (draft: TState) => void): void;
@@ -8,7 +8,7 @@ function target(step, fallback) {
8
8
  if ('step' in step)
9
9
  return { step: step.step };
10
10
  if ('complete' in step)
11
- return { step: fallback, action: { type: 'complete', result: step.complete.value ?? null, ...(step.complete.privacy === undefined ? {} : { privacy: step.complete.privacy }), ...(step.complete.children === undefined ? {} : { children: step.complete.children }) } };
11
+ return { step: fallback, action: { type: 'complete', result: step.complete.value ?? null, ...(step.complete.privacy === undefined ? {} : { privacy: step.complete.privacy }), ...(step.complete.derivedFrom === undefined ? {} : { derivedFrom: step.complete.derivedFrom }), ...(step.complete.children === undefined ? {} : { children: step.complete.children }) } };
12
12
  return { step: fallback, action: { type: 'fail', error: { code: step.fail.code, message: step.fail.message, ...(step.fail.retryable === undefined ? {} : { retryable: step.fail.retryable }), ...(step.fail.details === undefined ? {} : { details: step.fail.details }) }, ...(step.fail.privacy === undefined ? {} : { privacy: step.fail.privacy }), ...(step.fail.derivedFrom === undefined ? {} : { derivedFrom: step.fail.derivedFrom }) } };
13
13
  }
14
14
  function clone(value) { return structuredClone(value); }
@@ -318,7 +318,8 @@ function makeContext(context, initialState) {
318
318
  const ctx = {
319
319
  lane: context.lane, goal: context.lane.goal, global, globalVersion, laneState: readonlyState, history, now: context.now, ...(context.lane.progressWatchdog === undefined ? {} : { watchdog: context.lane.progressWatchdog }), ...(context.resumeInput ? { resumeInput: context.resumeInput } : {}), ...(context.humanInputs?.length ? { humanInputs: context.humanInputs } : {}),
320
320
  results: { meta: resultMeta, summary: (ref) => { if (context.state.results.has(ref) && resultVisible(context, ref))
321
- derivedRefs.add(ref); return resultMeta(ref)?.summary; } },
321
+ derivedRefs.add(ref); return resultMeta(ref)?.summary; }, read: (ref) => { if (context.state.results.has(ref) && resultVisible(context, ref))
322
+ derivedRefs.add(ref); return findResult(context, ref); } },
322
323
  mergeProposals: [...context.state.mergeProposals.values()].filter((proposal) => proposal.agentId === context.lane.agentId).map((proposal) => { for (const ref of proposal.delta.derivedFrom ?? [])
323
324
  derivedRefs.add(ref); return clone(proposal); }),
324
325
  mutateLane: (mutator) => { mutator((draftProxy?.draft ?? draft)); const changes = draftProxy?.changes(); delta = { target: 'lane', baseVersion: context.lane.context.version, ops: changes?.ops.map((op) => op.op === 'set' ? { op: 'set', path: op.path, value: asJson(op.value) } : op.op === 'append' ? { op: 'append', path: op.path, value: asJson(op.value) } : { op: 'remove', path: op.path }) ?? [] }; },
@@ -539,7 +540,13 @@ export class StepBuilder {
539
540
  if (finishReason === 'tool_calls') {
540
541
  if (turns >= maxTurns || toolCalls.length === 0)
541
542
  return maxTurnsReached();
542
- const invalidTool = toolCalls.find((call) => { const item = call && typeof call === 'object' && !Array.isArray(call) ? call : {}; const toolName = typeof item.name === 'string' ? item.name : ''; return !toolName || (options.toolAllow !== undefined && !options.toolAllow.includes(toolName)); });
543
+ const canonicalToolName = (name) => {
544
+ if (options.toolAllow === undefined || options.toolAllow.includes(name))
545
+ return name;
546
+ const aliases = options.toolAllow.filter((allowed) => allowed.replaceAll('.', '_') === name);
547
+ return aliases.length === 1 ? aliases[0] : undefined;
548
+ };
549
+ const invalidTool = toolCalls.find((call) => { const item = call && typeof call === 'object' && !Array.isArray(call) ? call : {}; const toolName = typeof item.name === 'string' ? item.name : ''; const canonical = canonicalToolName(toolName); return !toolName || canonical === undefined || (options.toolAllow !== undefined && !options.toolAllow.includes(canonical)); });
543
550
  if (invalidTool !== undefined)
544
551
  return fail({ code: 'ACTION_TOOL_NOT_ALLOWED', message: 'Model requested a tool outside the ReAct allow-list.', retryable: false });
545
552
  const resolution = waitResolution(ctx.resumeInput);
@@ -551,74 +558,76 @@ export class StepBuilder {
551
558
  const calls = toolCalls.map((call, index) => {
552
559
  const item = call && typeof call === 'object' && !Array.isArray(call) ? call : {};
553
560
  const originalId = typeof item.toolCallId === 'string' ? item.toolCallId : `call-${index + 1}`;
554
- const toolName = typeof item.name === 'string' ? item.name : '';
561
+ const rawToolName = typeof item.name === 'string' ? item.name : '';
562
+ const toolName = canonicalToolName(rawToolName);
555
563
  return { originalId, toolName, toolCallId: options.scopeToolCallsToEffect ? `${name}:${sourceEffectId ?? 'turn'}:${turns}:${originalId}` : `${name}:${turns}:${originalId}`, input: item.input ?? {} };
556
564
  });
557
565
  const askCalls = calls.filter((call) => String(call.toolName).startsWith('ask.'));
558
566
  if (askCalls.length > 0) {
559
567
  if (askCalls.length !== calls.length)
560
568
  return fail({ code: 'ASK_MIXED_TOOL_CALLS', message: 'An ask interaction must be requested in a separate model turn from workspace tools.', retryable: false });
561
- if (askCalls.length !== 1)
562
- return fail({ code: 'ASK_MULTIPLE_REQUESTS', message: 'Only one ask interaction may be requested at a time.', retryable: false });
563
- const call = askCalls[0];
564
- const rawInput = call.input && typeof call.input === 'object' && !Array.isArray(call.input) ? call.input : {};
565
- const askType = call.toolName === 'ask.choice' ? 'choice' : call.toolName === 'ask.multi' ? 'multi' : call.toolName === 'ask.input' ? 'input' : undefined;
566
- if (!askType)
567
- return fail({ code: 'ASK_TOOL_UNKNOWN', message: `Unknown ask tool ${String(call.toolName)}.`, retryable: false });
568
- if (typeof rawInput.prompt === 'string' && rawInput.prompt.length > 2_000)
569
- return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask prompts must be 2000 characters or fewer.', retryable: false });
570
- const prompt = typeof rawInput.prompt === 'string' && rawInput.prompt.trim() ? rawInput.prompt : `Pulse needs your input for ${askType}.`;
571
- const askInput = { kind: 'ask', type: askType, toolName: String(call.toolName), toolCallId: String(call.toolCallId), prompt };
572
- if (askType === 'choice' || askType === 'multi') {
573
- const rawOptions = Array.isArray(rawInput.options) ? rawInput.options : [];
574
- if (rawOptions.length > 50)
575
- return fail({ code: 'ASK_OPTIONS_TOO_MANY', message: 'ask.choice and ask.multi accept at most 50 options.', retryable: false });
576
- const seen = new Set();
577
- const options = rawOptions.flatMap((option) => {
578
- const item = typeof option === 'string' ? { label: option, value: option } : option && typeof option === 'object' && !Array.isArray(option) ? option : undefined;
579
- if (!item || typeof item.label !== 'string' || typeof item.value !== 'string')
580
- return [];
581
- if (item.label.length === 0 || item.label.length > 500 || item.value.length === 0 || item.value.length > 500 || seen.has(item.value))
582
- return [];
583
- seen.add(item.value);
584
- return [{ label: item.label, value: item.value }];
585
- });
586
- if (options.length === 0)
587
- return fail({ code: 'ASK_OPTIONS_REQUIRED', message: 'ask.choice and ask.multi require at least one valid option.', retryable: false });
588
- askInput.options = options;
589
- if (askType === 'multi') {
590
- const min = typeof rawInput.min === 'number' && Number.isInteger(rawInput.min) ? rawInput.min : undefined;
591
- const max = typeof rawInput.max === 'number' && Number.isInteger(rawInput.max) ? rawInput.max : undefined;
592
- if ((rawInput.min !== undefined && (min === undefined || min < 0)) || (rawInput.max !== undefined && (max === undefined || max < 1)) || (min !== undefined && max !== undefined && min > max))
593
- return fail({ code: 'ASK_RANGE_INVALID', message: 'ask.multi min and max must be integers with min <= max.', retryable: false });
594
- if (min !== undefined)
595
- askInput.min = min;
596
- if (max !== undefined)
597
- askInput.max = max;
598
- }
599
- }
600
- else {
601
- if (typeof rawInput.placeholder === 'string') {
602
- if (rawInput.placeholder.length > 500)
603
- return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask placeholders must be 500 characters or fewer.', retryable: false });
604
- askInput.placeholder = rawInput.placeholder;
569
+ const askEffects = [];
570
+ for (const [askIndex, call] of askCalls.entries()) {
571
+ const rawInput = call.input && typeof call.input === 'object' && !Array.isArray(call.input) ? call.input : {};
572
+ const askType = call.toolName === 'ask.choice' ? 'choice' : call.toolName === 'ask.multi' ? 'multi' : call.toolName === 'ask.input' ? 'input' : undefined;
573
+ if (!askType)
574
+ return fail({ code: 'ASK_TOOL_UNKNOWN', message: `Unknown ask tool ${String(call.toolName)}.`, retryable: false });
575
+ if (typeof rawInput.prompt === 'string' && rawInput.prompt.length > 2_000)
576
+ return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask prompts must be 2000 characters or fewer.', retryable: false });
577
+ const prompt = typeof rawInput.prompt === 'string' && rawInput.prompt.trim() ? rawInput.prompt : `Pulse needs your input for ${askType}.`;
578
+ const askInput = { kind: 'ask', type: askType, toolName: String(call.toolName), toolCallId: String(call.toolCallId), prompt };
579
+ if (askType === 'choice' || askType === 'multi') {
580
+ const rawOptions = Array.isArray(rawInput.options) ? rawInput.options : [];
581
+ if (rawOptions.length > 50)
582
+ return fail({ code: 'ASK_OPTIONS_TOO_MANY', message: 'ask.choice and ask.multi accept at most 50 options.', retryable: false });
583
+ const seen = new Set();
584
+ const options = rawOptions.flatMap((option) => {
585
+ const item = typeof option === 'string' ? { label: option, value: option } : option && typeof option === 'object' && !Array.isArray(option) ? option : undefined;
586
+ if (!item || typeof item.label !== 'string' || typeof item.value !== 'string')
587
+ return [];
588
+ if (item.label.length === 0 || item.label.length > 500 || item.value.length === 0 || item.value.length > 500 || seen.has(item.value))
589
+ return [];
590
+ seen.add(item.value);
591
+ return [{ label: item.label, value: item.value }];
592
+ });
593
+ if (options.length === 0)
594
+ return fail({ code: 'ASK_OPTIONS_REQUIRED', message: 'ask.choice and ask.multi require at least one valid option.', retryable: false });
595
+ askInput.options = options;
596
+ if (askType === 'multi') {
597
+ const min = typeof rawInput.min === 'number' && Number.isInteger(rawInput.min) ? rawInput.min : undefined;
598
+ const max = typeof rawInput.max === 'number' && Number.isInteger(rawInput.max) ? rawInput.max : undefined;
599
+ if ((rawInput.min !== undefined && (min === undefined || min < 0)) || (rawInput.max !== undefined && (max === undefined || max < 1)) || (min !== undefined && max !== undefined && min > max))
600
+ return fail({ code: 'ASK_RANGE_INVALID', message: 'ask.multi min and max must be integers with min <= max.', retryable: false });
601
+ if (min !== undefined)
602
+ askInput.min = min;
603
+ if (max !== undefined)
604
+ askInput.max = max;
605
+ }
605
606
  }
606
- if (typeof rawInput.defaultValue === 'string') {
607
- if (rawInput.defaultValue.length > 2_000)
608
- return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask default values must be 2000 characters or fewer.', retryable: false });
609
- askInput.defaultValue = rawInput.defaultValue;
607
+ else {
608
+ if (typeof rawInput.placeholder === 'string') {
609
+ if (rawInput.placeholder.length > 500)
610
+ return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask placeholders must be 500 characters or fewer.', retryable: false });
611
+ askInput.placeholder = rawInput.placeholder;
612
+ }
613
+ if (typeof rawInput.defaultValue === 'string') {
614
+ if (rawInput.defaultValue.length > 2_000)
615
+ return fail({ code: 'ASK_PROMPT_TOO_LARGE', message: 'Ask default values must be 2000 characters or fewer.', retryable: false });
616
+ askInput.defaultValue = rawInput.defaultValue;
617
+ }
610
618
  }
619
+ const humanEffect = {
620
+ key: `${name}-ask-${turns}-${askIndex + 1}`,
621
+ ...(sourceEffectId === undefined ? {} : { llmEffectId: sourceEffectId }),
622
+ ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }),
623
+ ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}),
624
+ kind: 'human',
625
+ concurrencyClass: 'none',
626
+ input: askInput,
627
+ };
628
+ askEffects.push(humanEffect);
611
629
  }
612
- const humanEffect = {
613
- key: `${name}-ask-${turns}`,
614
- ...(sourceEffectId === undefined ? {} : { llmEffectId: sourceEffectId }),
615
- ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }),
616
- ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}),
617
- kind: 'human',
618
- concurrencyClass: 'none',
619
- input: askInput,
620
- };
621
- return { actions: [{ type: 'submit_effects', effects: [humanEffect], wait: { onUnsatisfied: 'resume_with_error' } }], next: `${name}:tools`, locals: clearPendingResult(ctx) };
630
+ return { actions: [{ type: 'submit_effects', effects: askEffects, wait: { onUnsatisfied: 'resume_with_error' } }], next: `${name}:tools`, locals: clearPendingResult(ctx) };
622
631
  }
623
632
  const makeToolEffects = (approvedCalls) => ({ type: 'submit_effects', effects: approvedCalls.map((call, index) => ({ key: `${name}-tool-${turns}-${index + 1}`, toolCallId: String(call.toolCallId ?? `${name}:${turns}:${String(call.originalId)}`), ...(sourceEffectId === undefined ? {} : { llmEffectId: sourceEffectId }), ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }), ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}), kind: 'tool', concurrencyClass: 'tool', input: { toolCallId: String(call.toolCallId ?? `${name}:${turns}:${String(call.originalId)}`), name: String(call.toolName), arguments: call.input, ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }), ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}) } })), wait: { onUnsatisfied: 'resume_with_error' } });
624
633
  const dispatchTools = (action, locals) => {
@@ -1006,6 +1006,11 @@ export function validateStep(state, laneId, output) {
1006
1006
  ownerCopy.visibleResultRefs.add(resultId);
1007
1007
  else
1008
1008
  ownerCopy.visibleResultRefs = new Set([resultId]);
1009
+ for (const source of action.derivedFrom ?? []) {
1010
+ const ref = typeof source === 'string' ? (source.startsWith('result-') ? source : undefined) : source.kind === 'result' ? source.ref : undefined;
1011
+ if (ref)
1012
+ ownerCopy.visibleResultRefs.add(ref);
1013
+ }
1009
1014
  mutations.push({ op: 'setLane', laneId: owner.id, record: ownerCopy });
1010
1015
  }
1011
1016
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-runtime",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
+ "license": "PolyForm-Noncommercial-1.0.0",
4
5
  "repository": {
5
6
  "type": "git",
6
7
  "url": "https://github.com/zhuhengtan/Pulse"