@hunterzhu/pulse-runtime 0.3.0 → 0.4.0
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/dsl/program.d.ts +2 -0
- package/dist/dsl/program.js +71 -62
- package/dist/transitions/validate.js +5 -0
- package/package.json +1 -1
package/dist/dsl/program.d.ts
CHANGED
|
@@ -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;
|
package/dist/dsl/program.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
607
|
-
if (rawInput.
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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
|
}
|