jsfe 1.5.0 → 1.7.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/README.md +45 -4
- package/dist/forms/action-projection.d.ts +4 -0
- package/dist/forms/action-projection.d.ts.map +1 -0
- package/dist/forms/action-projection.js +21 -0
- package/dist/forms/action-recovery.d.ts +12 -0
- package/dist/forms/action-recovery.d.ts.map +1 -0
- package/dist/forms/action-recovery.js +125 -0
- package/dist/forms/action-tools.d.ts +29 -0
- package/dist/forms/action-tools.d.ts.map +1 -0
- package/dist/forms/action-tools.js +6 -0
- package/dist/forms/address-flows.d.ts +2 -0
- package/dist/forms/address-flows.d.ts.map +1 -0
- package/dist/forms/address-flows.js +39 -0
- package/dist/forms/choice-flow.d.ts +419 -0
- package/dist/forms/choice-flow.d.ts.map +1 -0
- package/dist/forms/choice-flow.js +707 -0
- package/dist/forms/contracts.d.ts +237 -0
- package/dist/forms/contracts.d.ts.map +1 -0
- package/dist/forms/contracts.js +278 -0
- package/dist/forms/flows.d.ts +611 -0
- package/dist/forms/flows.d.ts.map +1 -0
- package/dist/forms/flows.js +675 -0
- package/dist/forms/identity-flows.d.ts +17 -0
- package/dist/forms/identity-flows.d.ts.map +1 -0
- package/dist/forms/identity-flows.js +302 -0
- package/dist/forms/index.d.ts +7 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +10 -0
- package/dist/forms/prompt-compiler.d.ts +3 -0
- package/dist/forms/prompt-compiler.d.ts.map +1 -0
- package/dist/forms/prompt-compiler.js +30 -0
- package/dist/helpers/address.d.ts +27 -0
- package/dist/helpers/address.d.ts.map +1 -0
- package/dist/helpers/address.js +29 -0
- package/dist/helpers/answer.d.ts +5 -0
- package/dist/helpers/answer.d.ts.map +1 -1
- package/dist/helpers/answer.js +5 -3
- package/dist/helpers/choice-offer.d.ts +3 -0
- package/dist/helpers/choice-offer.d.ts.map +1 -0
- package/dist/helpers/choice-offer.js +6 -0
- package/dist/helpers/form-actions.d.ts +13 -0
- package/dist/helpers/form-actions.d.ts.map +1 -0
- package/dist/helpers/form-actions.js +144 -0
- package/dist/helpers/form-sources.d.ts +7 -0
- package/dist/helpers/form-sources.d.ts.map +1 -0
- package/dist/helpers/form-sources.js +47 -0
- package/dist/helpers/form-state.d.ts +94 -0
- package/dist/helpers/form-state.d.ts.map +1 -0
- package/dist/helpers/form-state.js +870 -0
- package/dist/helpers/index.d.ts +114 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +9 -4
- package/dist/helpers/match-inputs.d.ts +14 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +154 -41
- package/dist/helpers/money.d.ts +12 -0
- package/dist/helpers/money.d.ts.map +1 -0
- package/dist/helpers/money.js +54 -0
- package/dist/helpers/spoken-money.d.ts +20 -0
- package/dist/helpers/spoken-money.d.ts.map +1 -0
- package/dist/helpers/spoken-money.js +143 -0
- package/dist/host/action-journal.d.ts +98 -0
- package/dist/host/action-journal.d.ts.map +1 -0
- package/dist/host/action-journal.js +218 -0
- package/dist/host/action-session.d.ts +12 -0
- package/dist/host/action-session.d.ts.map +1 -0
- package/dist/host/action-session.js +29 -0
- package/dist/host/capabilities.d.ts +12 -0
- package/dist/host/capabilities.d.ts.map +1 -0
- package/dist/host/capabilities.js +43 -0
- package/dist/host/checkpoint-codec-error.d.ts +6 -0
- package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
- package/dist/host/checkpoint-codec-error.js +4 -0
- package/dist/host/index.d.ts +7 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +7 -0
- package/dist/host/intent-stages.d.ts +155 -0
- package/dist/host/intent-stages.d.ts.map +1 -0
- package/dist/host/intent-stages.js +256 -0
- package/dist/host/node.d.ts +17 -0
- package/dist/host/node.d.ts.map +1 -0
- package/dist/host/node.js +64 -0
- package/dist/host/recovery-identity.d.ts +16 -0
- package/dist/host/recovery-identity.d.ts.map +1 -0
- package/dist/host/recovery-identity.js +73 -0
- package/dist/host/recovery-policy.d.ts +5 -0
- package/dist/host/recovery-policy.d.ts.map +1 -0
- package/dist/host/recovery-policy.js +54 -0
- package/dist/host/runtime-manifest.d.ts +3 -0
- package/dist/host/runtime-manifest.d.ts.map +1 -0
- package/dist/host/runtime-manifest.js +38 -0
- package/dist/index.d.ts +75 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +239 -46
- package/dist/runtime-identity.d.ts +3 -0
- package/dist/runtime-identity.d.ts.map +1 -0
- package/dist/runtime-identity.js +1 -0
- package/docs/development-release.md +23 -0
- package/docs/forms.md +122 -0
- package/package.json +15 -4
package/dist/host/index.js
CHANGED
|
@@ -7,3 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export * from './request.js';
|
|
9
9
|
export * from './systemone-intent.js';
|
|
10
|
+
export * from './intent-stages.js';
|
|
11
|
+
export { createActionJournal, createMemoryActionStorage, ActionJournalError } from './action-journal.js';
|
|
12
|
+
export { actionSessionSnapshot, checkpointActionSession } from './action-session.js';
|
|
13
|
+
export { retainRecoveryState, applyRecoveryStatePolicy } from './recovery-policy.js';
|
|
14
|
+
export { CheckpointCodecError } from './checkpoint-codec-error.js';
|
|
15
|
+
export { createActionRecoveryIdentity, assertActionRecoveryIdentity, actionRecoveryIdentityMatches } from './recovery-identity.js';
|
|
16
|
+
export { assertFlowHostCapabilities, JSFEHostCapabilityError } from './capabilities.js';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-stage intent detection — the instructions and the decoders, zero runtime imports.
|
|
3
|
+
*
|
|
4
|
+
* Stage 1 (task 'detect_flow'): which primary flow, if any, the latest message starts.
|
|
5
|
+
* Stage 2 (task 'extract_parameters'): the values the user gave for the SELECTED flow's parameters.
|
|
6
|
+
*
|
|
7
|
+
* For each stage jsfe builds ONE instruction set per executor — `llm` (a text model) and `systemOne`
|
|
8
|
+
* (TypeSafe System One) — from the same rules and the same flow fields, and passes both to aiCallback as
|
|
9
|
+
* its 4th argument. The host is the executor: it runs either, or both as a hedge (the first complete
|
|
10
|
+
* answer wins and the other is cancelled), and returns the CANONICAL reply as a JSON string:
|
|
11
|
+
* stage 1: { "flowName": "<name>" | "None" }
|
|
12
|
+
* stage 2: { "parameters": { "<name>": value, … } }
|
|
13
|
+
* The decoders below turn either executor's raw answer into that reply, so the host never interprets one.
|
|
14
|
+
*
|
|
15
|
+
* What each stage reads, and nothing else:
|
|
16
|
+
* stage 1: each primary flow's `description` (what should trigger it);
|
|
17
|
+
* stage 2: the selected flow's `parameterInstructions` (optional: rules that span its parameters) and its
|
|
18
|
+
* parameters — name, type, enum and `description` (that parameter's rule). Parameters marked
|
|
19
|
+
* `internal: true` (or whose description begins "Internal") are the flow's own, never asked.
|
|
20
|
+
* System One answers only closed values (enum, boolean): its stage-2 instructions cover those parameters,
|
|
21
|
+
* so a flow with any free-text parameter is answered completely only by the LLM.
|
|
22
|
+
*/
|
|
23
|
+
import type { FlowParameterDefinition } from '../index.js';
|
|
24
|
+
/** A noul at or above this is a yes. */
|
|
25
|
+
export declare const STAGE_YES_THRESHOLD = 0.5;
|
|
26
|
+
/** Stage 1, System One: two top flows that together reach this pick the higher one (prod 10-17). */
|
|
27
|
+
export declare const STAGE_PAIR_SUM = 0.7;
|
|
28
|
+
export interface StageTurn {
|
|
29
|
+
role: 'user' | 'assistant';
|
|
30
|
+
content: string;
|
|
31
|
+
}
|
|
32
|
+
export interface StageFlow {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
}
|
|
37
|
+
export interface StageParameter {
|
|
38
|
+
name: string;
|
|
39
|
+
type?: string;
|
|
40
|
+
enum?: string[];
|
|
41
|
+
description?: string;
|
|
42
|
+
internal?: boolean;
|
|
43
|
+
jsonSchema?: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
export interface StageLlm {
|
|
46
|
+
system: string;
|
|
47
|
+
user: string;
|
|
48
|
+
schema: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
export interface StageSystemOne<I> {
|
|
51
|
+
state: Record<string, unknown>;
|
|
52
|
+
questions: Record<string, unknown>;
|
|
53
|
+
index: I[];
|
|
54
|
+
}
|
|
55
|
+
export interface FlowQuestionRef {
|
|
56
|
+
id: string;
|
|
57
|
+
flow: number;
|
|
58
|
+
}
|
|
59
|
+
export type ParameterQuestionRef = {
|
|
60
|
+
id: string;
|
|
61
|
+
kind: 'enum';
|
|
62
|
+
param: string;
|
|
63
|
+
value: string;
|
|
64
|
+
} | {
|
|
65
|
+
id: string;
|
|
66
|
+
kind: 'boolean';
|
|
67
|
+
param: string;
|
|
68
|
+
};
|
|
69
|
+
export interface FlowStageRequest {
|
|
70
|
+
task: 'detect_flow';
|
|
71
|
+
stage: 1;
|
|
72
|
+
input: string;
|
|
73
|
+
conversation: StageTurn[];
|
|
74
|
+
flows: StageFlow[];
|
|
75
|
+
llm: StageLlm;
|
|
76
|
+
systemOne: StageSystemOne<FlowQuestionRef>;
|
|
77
|
+
}
|
|
78
|
+
export interface ParameterStageRequest {
|
|
79
|
+
task: 'extract_parameters';
|
|
80
|
+
stage: 2;
|
|
81
|
+
input: string;
|
|
82
|
+
conversation: StageTurn[];
|
|
83
|
+
flow: {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
parameterInstructions?: string;
|
|
87
|
+
parameters: StageParameter[];
|
|
88
|
+
};
|
|
89
|
+
llm: StageLlm;
|
|
90
|
+
/** null when the flow declares no enum or boolean parameter */
|
|
91
|
+
systemOne: StageSystemOne<ParameterQuestionRef> | null;
|
|
92
|
+
/** parameters only a text model can answer (free text) */
|
|
93
|
+
freeText: string[];
|
|
94
|
+
}
|
|
95
|
+
export type IntentStageRequest = FlowStageRequest | ParameterStageRequest;
|
|
96
|
+
/** System One's answers, keyed by question id. */
|
|
97
|
+
export type StageSystemOneAnswers = Record<string, {
|
|
98
|
+
noul?: unknown;
|
|
99
|
+
} | undefined> | undefined;
|
|
100
|
+
/** The flow's own parameters, never asked of the user. */
|
|
101
|
+
export declare const isInternalParameter: (p: StageParameter | FlowParameterDefinition) => boolean;
|
|
102
|
+
/** Stage 1's instructions for both executors. `flows` are the primaries offered. */
|
|
103
|
+
export declare function buildFlowStage(input: string, conversation: StageTurn[], flows: StageFlow[]): FlowStageRequest;
|
|
104
|
+
/** A text model's stage-1 answer as the canonical reply; throws when it names no offered flow. */
|
|
105
|
+
export declare function decodeFlowLlm(text: string, stage: FlowStageRequest): {
|
|
106
|
+
flowName: string;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* System One's stage-1 answers as the canonical reply, with the top three scores (for logs and a host's hedge
|
|
110
|
+
* policy) and `pair` when two close candidates picked the higher. Throws if any question is unanswered.
|
|
111
|
+
*/
|
|
112
|
+
export declare function decodeFlowSystemOne(answers: StageSystemOneAnswers, stage: FlowStageRequest, { threshold, pairSum }?: {
|
|
113
|
+
threshold?: number | undefined;
|
|
114
|
+
pairSum?: number | undefined;
|
|
115
|
+
}): {
|
|
116
|
+
flowName: string;
|
|
117
|
+
scores: {
|
|
118
|
+
name: string;
|
|
119
|
+
p: number;
|
|
120
|
+
}[];
|
|
121
|
+
pair: boolean;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A System One "None" that is a near miss: the chance that at least one of its two top flows applies,
|
|
125
|
+
* 1 − (1 − p1)(1 − p2), clears the threshold — a flow is wanted and the doubt is which. A host hedge waits
|
|
126
|
+
* for the other executor on it rather than accepting "None".
|
|
127
|
+
*/
|
|
128
|
+
export declare function isNearMiss(scores: {
|
|
129
|
+
p: number;
|
|
130
|
+
}[], threshold?: number): boolean;
|
|
131
|
+
/** Does `parameters` hold anything stage 2 asks (a parameter that is not internal)? */
|
|
132
|
+
export declare function hasStageParameters(parameters: (StageParameter | FlowParameterDefinition)[] | undefined): boolean;
|
|
133
|
+
/** Stage 2's instructions for the selected flow, for both executors. */
|
|
134
|
+
export declare function buildParameterStage(input: string, conversation: StageTurn[], flow: {
|
|
135
|
+
id: string;
|
|
136
|
+
name: string;
|
|
137
|
+
parameterInstructions?: string;
|
|
138
|
+
parameters?: (StageParameter | FlowParameterDefinition)[];
|
|
139
|
+
}): ParameterStageRequest;
|
|
140
|
+
/** A text model's stage-2 answer as the canonical reply: declared parameters with a value only. Throws on non-JSON. */
|
|
141
|
+
export declare function decodeParametersLlm(text: string, stage: ParameterStageRequest): {
|
|
142
|
+
parameters: Record<string, unknown>;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* System One's stage-2 answers as the canonical reply: an enum takes its argmax value if it clears the cut, a
|
|
146
|
+
* boolean is true if it clears it; otherwise the parameter is omitted (the flow asks), never set false.
|
|
147
|
+
*/
|
|
148
|
+
export declare function decodeParametersSystemOne(answers: StageSystemOneAnswers, stage: ParameterStageRequest, { threshold }?: {
|
|
149
|
+
threshold?: number | undefined;
|
|
150
|
+
}): {
|
|
151
|
+
parameters: Record<string, unknown>;
|
|
152
|
+
};
|
|
153
|
+
/** Is this jsfe's stage request (a host's aiCallback receives it as the 4th argument)? */
|
|
154
|
+
export declare function isIntentStageRequest(request: unknown): request is IntentStageRequest;
|
|
155
|
+
//# sourceMappingURL=intent-stages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intent-stages.d.ts","sourceRoot":"","sources":["../../src/host/intent-stages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-stage intent detection — the instructions and the decoders, zero runtime imports.
|
|
3
|
+
*
|
|
4
|
+
* Stage 1 (task 'detect_flow'): which primary flow, if any, the latest message starts.
|
|
5
|
+
* Stage 2 (task 'extract_parameters'): the values the user gave for the SELECTED flow's parameters.
|
|
6
|
+
*
|
|
7
|
+
* For each stage jsfe builds ONE instruction set per executor — `llm` (a text model) and `systemOne`
|
|
8
|
+
* (TypeSafe System One) — from the same rules and the same flow fields, and passes both to aiCallback as
|
|
9
|
+
* its 4th argument. The host is the executor: it runs either, or both as a hedge (the first complete
|
|
10
|
+
* answer wins and the other is cancelled), and returns the CANONICAL reply as a JSON string:
|
|
11
|
+
* stage 1: { "flowName": "<name>" | "None" }
|
|
12
|
+
* stage 2: { "parameters": { "<name>": value, … } }
|
|
13
|
+
* The decoders below turn either executor's raw answer into that reply, so the host never interprets one.
|
|
14
|
+
*
|
|
15
|
+
* What each stage reads, and nothing else:
|
|
16
|
+
* stage 1: each primary flow's `description` (what should trigger it);
|
|
17
|
+
* stage 2: the selected flow's `parameterInstructions` (optional: rules that span its parameters) and its
|
|
18
|
+
* parameters — name, type, enum and `description` (that parameter's rule). Parameters marked
|
|
19
|
+
* `internal: true` (or whose description begins "Internal") are the flow's own, never asked.
|
|
20
|
+
* System One answers only closed values (enum, boolean): its stage-2 instructions cover those parameters,
|
|
21
|
+
* so a flow with any free-text parameter is answered completely only by the LLM.
|
|
22
|
+
*/
|
|
23
|
+
/** A noul at or above this is a yes. */
|
|
24
|
+
export const STAGE_YES_THRESHOLD = 0.5;
|
|
25
|
+
/** Stage 1, System One: two top flows that together reach this pick the higher one (prod 10-17). */
|
|
26
|
+
export const STAGE_PAIR_SUM = 0.7;
|
|
27
|
+
// ── the rules: written once, rendered for both executors ─────────────────────────────────────────────
|
|
28
|
+
const FLOW_TRUE = "The latest message, read with the conversation, is what the workflow's description says should start it: the user asks for its action or information, or raises the problem it exists to handle. Also when it accepts the assistant's offer in the conversation to start or resume it (assistant: \"Shall I try the payment again?\" → user: \"yes\").";
|
|
29
|
+
const FLOW_FALSE = "It only shares a topic with the workflow, is not what the workflow's description covers, or is a greeting, an acknowledgement or a fragment that asks for nothing.";
|
|
30
|
+
const PARAMETER_RULES = [
|
|
31
|
+
'Give a value only when the user gave it, in the latest message or earlier in the conversation. Never guess.',
|
|
32
|
+
"Write it as the parameter's rule says; otherwise the value alone, in the user's words (never the whole sentence around it).",
|
|
33
|
+
'A short answer to the assistant\'s last message ("1", "yes", "the first one") chooses what that message offered.',
|
|
34
|
+
'A value the user did not give is null.',
|
|
35
|
+
];
|
|
36
|
+
const ENUM_TRUE = (value) => `The parameter's rule makes \`${value}\` its value for what the user said.`;
|
|
37
|
+
const ENUM_FALSE = 'Another value, or none, is its value; or the user has not said enough to tell.';
|
|
38
|
+
const BOOLEAN_TRUE = "The parameter's rule makes it true for what the user said.";
|
|
39
|
+
const BOOLEAN_FALSE = 'The rule does not make it true, or the user has not said enough to tell.';
|
|
40
|
+
const isEnum = (p) => Array.isArray(p.enum) && p.enum.length > 0;
|
|
41
|
+
const isBoolean = (p) => p.type === 'boolean';
|
|
42
|
+
/** The flow's own parameters, never asked of the user. */
|
|
43
|
+
export const isInternalParameter = (p) => p.internal === true || /^\s*internal\b/i.test(String(p.description ?? ''));
|
|
44
|
+
const conversationText = (conversation, input) => `${conversation.length ? `<conversation>\n${conversation.map(t => `${t.role}: ${t.content}`).join('\n')}\n</conversation>\n` : ''}<latest-message>\n${input}\n</latest-message>`;
|
|
45
|
+
const stateOf = (conversation, input, instructions) => ({
|
|
46
|
+
assistant_instructions: instructions,
|
|
47
|
+
conversation: conversation.map(t => ({ role: t.role, content: t.content })),
|
|
48
|
+
latest_message: input,
|
|
49
|
+
});
|
|
50
|
+
const jsonTail = (schema) => `\n\n<json-schema>\n${JSON.stringify(schema)}\n</json-schema>\n\nRespond ONLY with valid JSON matching the schema. No other text.`;
|
|
51
|
+
// ── stage 1 ───────────────────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
/** Stage 1's instructions for both executors. `flows` are the primaries offered. */
|
|
53
|
+
export function buildFlowStage(input, conversation, flows) {
|
|
54
|
+
const names = flows.map(f => f.name);
|
|
55
|
+
const schema = {
|
|
56
|
+
type: 'object', additionalProperties: false, required: ['flowName'],
|
|
57
|
+
properties: { flowName: { type: 'string', enum: [...names, 'None'] } },
|
|
58
|
+
};
|
|
59
|
+
const system = [
|
|
60
|
+
'Decide which workflow, if any, the latest message starts.',
|
|
61
|
+
`A workflow is chosen when: ${FLOW_TRUE}`,
|
|
62
|
+
`"None" when: ${FLOW_FALSE}`,
|
|
63
|
+
'Answer with the exact workflow name, or "None".',
|
|
64
|
+
'',
|
|
65
|
+
'<workflows>',
|
|
66
|
+
...flows.map(f => `- ${f.name}: ${f.description}`),
|
|
67
|
+
'</workflows>',
|
|
68
|
+
].join('\n') + jsonTail(schema);
|
|
69
|
+
const questions = {};
|
|
70
|
+
const index = [];
|
|
71
|
+
flows.forEach((f, i) => {
|
|
72
|
+
const id = `f${i}`;
|
|
73
|
+
questions[id] = {
|
|
74
|
+
type: 'noul', criteria: { true: FLOW_TRUE, false: FLOW_FALSE },
|
|
75
|
+
instructions: { flow: { name: f.name, description: f.description }, question: 'Should the latest message start the workflow in `flow`?' },
|
|
76
|
+
};
|
|
77
|
+
index.push({ id, flow: i });
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
task: 'detect_flow', stage: 1, input, conversation, flows,
|
|
81
|
+
llm: { system, user: conversationText(conversation, input), schema },
|
|
82
|
+
systemOne: { state: stateOf(conversation, input, 'Decide which workflow, if any, the latest message starts.'), questions, index },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** A text model's stage-1 answer as the canonical reply; throws when it names no offered flow. */
|
|
86
|
+
export function decodeFlowLlm(text, stage) {
|
|
87
|
+
const parsed = parseJsonObject(text);
|
|
88
|
+
const said = String(parsed.flowName ?? '').trim();
|
|
89
|
+
if (/^(none|null)$/i.test(said)) {
|
|
90
|
+
return { flowName: 'None' };
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const flow = stage.flows.find(f => f.name.toLowerCase() === said.toLowerCase());
|
|
94
|
+
if (!flow)
|
|
95
|
+
throw new Error(`stage 1: "${said.slice(0, 60)}" is not an offered workflow`);
|
|
96
|
+
return { flowName: flow.name };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* System One's stage-1 answers as the canonical reply, with the top three scores (for logs and a host's hedge
|
|
101
|
+
* policy) and `pair` when two close candidates picked the higher. Throws if any question is unanswered.
|
|
102
|
+
*/
|
|
103
|
+
export function decodeFlowSystemOne(answers, stage, { threshold = STAGE_YES_THRESHOLD, pairSum = STAGE_PAIR_SUM } = {}) {
|
|
104
|
+
const p = answered(answers, stage.systemOne.index);
|
|
105
|
+
const ranked = stage.systemOne.index.map(q => ({ name: stage.flows[q.flow].name, p: p.get(q.id) })).sort((a, b) => b.p - a.p);
|
|
106
|
+
const scores = ranked.slice(0, 3);
|
|
107
|
+
const [a, b] = ranked;
|
|
108
|
+
if (a && a.p >= threshold) {
|
|
109
|
+
return { flowName: a.name, scores, pair: false };
|
|
110
|
+
}
|
|
111
|
+
else if (a && b && a.p + b.p >= pairSum) {
|
|
112
|
+
return { flowName: a.name, scores, pair: true };
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
return { flowName: 'None', scores, pair: false };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A System One "None" that is a near miss: the chance that at least one of its two top flows applies,
|
|
120
|
+
* 1 − (1 − p1)(1 − p2), clears the threshold — a flow is wanted and the doubt is which. A host hedge waits
|
|
121
|
+
* for the other executor on it rather than accepting "None".
|
|
122
|
+
*/
|
|
123
|
+
export function isNearMiss(scores, threshold = STAGE_YES_THRESHOLD) {
|
|
124
|
+
const [a = 0, b = 0] = (scores || []).map(s => Number(s?.p) || 0).sort((x, y) => y - x);
|
|
125
|
+
return a > 0 && 1 - (1 - a) * (1 - b) >= threshold;
|
|
126
|
+
}
|
|
127
|
+
// ── stage 2 ───────────────────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
/** Does `parameters` hold anything stage 2 asks (a parameter that is not internal)? */
|
|
129
|
+
export function hasStageParameters(parameters) {
|
|
130
|
+
return (parameters || []).some(p => p && p.name && !isInternalParameter(p));
|
|
131
|
+
}
|
|
132
|
+
/** Stage 2's instructions for the selected flow, for both executors. */
|
|
133
|
+
export function buildParameterStage(input, conversation, flow) {
|
|
134
|
+
const params = (flow.parameters || []).filter(p => p && p.name && !isInternalParameter(p))
|
|
135
|
+
.map(p => ({ name: p.name, ...(p.type ? { type: p.type } : {}), ...(Array.isArray(p.enum) ? { enum: [...p.enum] } : {}), description: String(p.description ?? ''), ...(p.jsonSchema ? { jsonSchema: structuredClone(p.jsonSchema) } : {}) }));
|
|
136
|
+
const typeOf = (p) => (p.type === 'boolean' ? 'boolean' : p.type === 'number' || p.type === 'integer' ? 'number' : 'string');
|
|
137
|
+
const schema = {
|
|
138
|
+
type: 'object', additionalProperties: false, required: params.map(p => p.name),
|
|
139
|
+
properties: Object.fromEntries(params.map(p => [p.name, p.jsonSchema ? { anyOf: [p.jsonSchema, { type: 'null' }] } : isEnum(p) ? { type: ['string', 'null'], enum: [...p.enum, null] } : { type: [typeOf(p), 'null'] }])),
|
|
140
|
+
};
|
|
141
|
+
const line = (p) => `- ${p.name} (${isEnum(p) ? `one of: ${p.enum.join(' | ')}` : p.jsonSchema ? p.type || 'object' : typeOf(p)}): ${p.description}`;
|
|
142
|
+
const instructions = String(flow.parameterInstructions ?? '').trim();
|
|
143
|
+
const rules = [
|
|
144
|
+
`The workflow ${flow.name} was chosen. Extract the values the user gave for its parameters.`,
|
|
145
|
+
...PARAMETER_RULES.map(r => `- ${r}`),
|
|
146
|
+
...(instructions ? ['', instructions] : []),
|
|
147
|
+
].join('\n');
|
|
148
|
+
const system = `${rules}\n\n<parameters>\n${params.map(line).join('\n')}\n</parameters>` + jsonTail(schema);
|
|
149
|
+
const closed = params.filter(p => isEnum(p) || isBoolean(p));
|
|
150
|
+
let systemOne = null;
|
|
151
|
+
if (closed.length) {
|
|
152
|
+
const questions = {};
|
|
153
|
+
const index = [];
|
|
154
|
+
closed.forEach((p, k) => {
|
|
155
|
+
if (isEnum(p)) {
|
|
156
|
+
p.enum.forEach((value, v) => {
|
|
157
|
+
const id = `p${k}v${v}`;
|
|
158
|
+
questions[id] = { type: 'noul', criteria: { true: ENUM_TRUE(value), false: ENUM_FALSE }, instructions: { parameter: { name: p.name }, candidate_value: value, question: `Is \`${value}\` the value of \`${p.name}\`?` } };
|
|
159
|
+
index.push({ id, kind: 'enum', param: p.name, value });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const id = `p${k}`;
|
|
164
|
+
questions[id] = { type: 'noul', criteria: { true: BOOLEAN_TRUE, false: BOOLEAN_FALSE }, instructions: { parameter: { name: p.name }, question: `Is \`${p.name}\` true?` } };
|
|
165
|
+
index.push({ id, kind: 'boolean', param: p.name });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const ruleSet = Object.fromEntries(closed.map(p => [p.name, p.description]));
|
|
169
|
+
systemOne = { state: stateOf(conversation, input, `${rules}\n\nEach question asks for one parameter. The rule of each parameter, by name:\n${JSON.stringify(ruleSet, null, 1)}`), questions, index };
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// no closed value to classify
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
task: 'extract_parameters', stage: 2, input, conversation,
|
|
176
|
+
flow: { id: flow.id, name: flow.name, ...(instructions ? { parameterInstructions: instructions } : {}), parameters: params },
|
|
177
|
+
llm: { system, user: conversationText(conversation, input), schema },
|
|
178
|
+
systemOne,
|
|
179
|
+
freeText: params.filter(p => !isEnum(p) && !isBoolean(p)).map(p => p.name),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/** A text model's stage-2 answer as the canonical reply: declared parameters with a value only. Throws on non-JSON. */
|
|
183
|
+
export function decodeParametersLlm(text, stage) {
|
|
184
|
+
const parsed = parseJsonObject(text);
|
|
185
|
+
const out = {};
|
|
186
|
+
for (const p of stage.flow.parameters) {
|
|
187
|
+
const v = parsed[p.name];
|
|
188
|
+
if (v === null || v === undefined || (typeof v === 'string' && (v.trim() === '' || /^(null|none|n\/a)$/i.test(v.trim())))) {
|
|
189
|
+
continue; // not given
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
out[p.name] = typeof v === 'string' ? v.trim() : v;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { parameters: out };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* System One's stage-2 answers as the canonical reply: an enum takes its argmax value if it clears the cut, a
|
|
199
|
+
* boolean is true if it clears it; otherwise the parameter is omitted (the flow asks), never set false.
|
|
200
|
+
*/
|
|
201
|
+
export function decodeParametersSystemOne(answers, stage, { threshold = STAGE_YES_THRESHOLD } = {}) {
|
|
202
|
+
if (!stage.systemOne)
|
|
203
|
+
return { parameters: {} };
|
|
204
|
+
const p = answered(answers, stage.systemOne.index);
|
|
205
|
+
const parameters = {};
|
|
206
|
+
const best = new Map();
|
|
207
|
+
for (const q of stage.systemOne.index) {
|
|
208
|
+
const prob = p.get(q.id);
|
|
209
|
+
if (q.kind === 'boolean') {
|
|
210
|
+
if (prob >= threshold)
|
|
211
|
+
parameters[q.param] = true;
|
|
212
|
+
else { /* omitted */ }
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const cur = best.get(q.param);
|
|
216
|
+
if (!cur || prob > cur.p)
|
|
217
|
+
best.set(q.param, { value: q.value, p: prob });
|
|
218
|
+
else { /* lower */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const [param, { value, p: prob }] of best) {
|
|
222
|
+
if (prob >= threshold)
|
|
223
|
+
parameters[param] = value;
|
|
224
|
+
else { /* omitted: the flow asks */ }
|
|
225
|
+
}
|
|
226
|
+
return { parameters };
|
|
227
|
+
}
|
|
228
|
+
/** Is this jsfe's stage request (a host's aiCallback receives it as the 4th argument)? */
|
|
229
|
+
export function isIntentStageRequest(request) {
|
|
230
|
+
const r = request;
|
|
231
|
+
return !!r && ((r.task === 'detect_flow' && r.stage === 1) || (r.task === 'extract_parameters' && r.stage === 2));
|
|
232
|
+
}
|
|
233
|
+
// ── shared ────────────────────────────────────────────────────────────────────────────────────────────
|
|
234
|
+
function parseJsonObject(text) {
|
|
235
|
+
const t = String(text ?? '').replace(/<think>[\s\S]*?<\/think>/g, '').replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
|
|
236
|
+
let parsed;
|
|
237
|
+
try {
|
|
238
|
+
parsed = JSON.parse((t.match(/\{[\s\S]*\}/) || [t])[0]);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
throw new Error(`not JSON: ${t.slice(0, 60)}`);
|
|
242
|
+
}
|
|
243
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
244
|
+
throw new Error('not a JSON object');
|
|
245
|
+
return parsed;
|
|
246
|
+
}
|
|
247
|
+
function answered(answers, index) {
|
|
248
|
+
const p = new Map();
|
|
249
|
+
for (const q of index) {
|
|
250
|
+
const noul = Number(answers?.[q.id]?.noul);
|
|
251
|
+
if (!Number.isFinite(noul))
|
|
252
|
+
throw new Error(`System One: question ${q.id} unanswered`);
|
|
253
|
+
p.set(q.id, noul);
|
|
254
|
+
}
|
|
255
|
+
return p;
|
|
256
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function isEncodedCheckpoint(value: any): boolean;
|
|
2
|
+
export function encodeCheckpoint(value: any, overrides?: {}): Promise<{
|
|
3
|
+
format: string;
|
|
4
|
+
version: number;
|
|
5
|
+
encoding: string;
|
|
6
|
+
decodedBytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
data: Buffer<ArrayBufferLike>;
|
|
9
|
+
}>;
|
|
10
|
+
export function decodeCheckpoint(envelope: any, overrides?: {}): Promise<any>;
|
|
11
|
+
export { verifyRuntimeFingerprint } from "./runtime-manifest.js";
|
|
12
|
+
export { CheckpointCodecError } from "./checkpoint-codec-error.js";
|
|
13
|
+
export const CHECKPOINT_CODEC_LIMITS: Readonly<{
|
|
14
|
+
maxEncodedBytes: number;
|
|
15
|
+
maxDecodedBytes: number;
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/host/node.js"],"names":[],"mappings":"AAUA,yDAAuN;AACvN;;;;;;;GAUC;AACD,8EAWC;;;AA3BD;;;GAA4G"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Node-only opaque JSON storage codec. Universal jsfe/host never imports this entry. */
|
|
2
|
+
export { verifyRuntimeFingerprint } from './runtime-manifest.js';
|
|
3
|
+
import { brotliCompress as compress, brotliDecompress as decompress, constants } from 'node:zlib';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { CheckpointCodecError } from './checkpoint-codec-error.js';
|
|
6
|
+
export { CheckpointCodecError } from './checkpoint-codec-error.js';
|
|
7
|
+
export const CHECKPOINT_CODEC_LIMITS = Object.freeze({ maxEncodedBytes: 350 * 1024, maxDecodedBytes: 16 * 1024 * 1024 });
|
|
8
|
+
const FORMAT = 'jsfe-checkpoint', brotliCompress = promisify(compress), brotliDecompress = promisify(decompress);
|
|
9
|
+
const options = overrides => { const limits = { ...CHECKPOINT_CODEC_LIMITS, ...overrides }; for (const name of ['maxEncodedBytes', 'maxDecodedBytes'])
|
|
10
|
+
if (!Number.isSafeInteger(limits[name]) || limits[name] < 1)
|
|
11
|
+
throw new CheckpointCodecError('invalid_' + name); return limits; };
|
|
12
|
+
const hash = async (bytes) => Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bytes)), b => b.toString(16).padStart(2, '0')).join('');
|
|
13
|
+
export function isEncodedCheckpoint(value) { return !!value && typeof value === 'object' && (Object.hasOwn(value, 'format') || Object.hasOwn(value, 'encoding') || Object.hasOwn(value, 'sha256') || value.data instanceof Uint8Array); }
|
|
14
|
+
export async function encodeCheckpoint(value, overrides = {}) {
|
|
15
|
+
const limits = options(overrides);
|
|
16
|
+
let json;
|
|
17
|
+
try {
|
|
18
|
+
json = JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new CheckpointCodecError('invalid_json_value');
|
|
22
|
+
}
|
|
23
|
+
if (typeof json !== 'string')
|
|
24
|
+
throw new CheckpointCodecError('invalid_json_value');
|
|
25
|
+
const raw = new TextEncoder().encode(json);
|
|
26
|
+
if (raw.byteLength > limits.maxDecodedBytes)
|
|
27
|
+
throw new CheckpointCodecError('decoded_size_limit');
|
|
28
|
+
let data;
|
|
29
|
+
try {
|
|
30
|
+
data = await brotliCompress(raw, { maxOutputLength: limits.maxEncodedBytes, params: { [constants.BROTLI_PARAM_QUALITY]: 4, [constants.BROTLI_PARAM_LGWIN]: 24, [constants.BROTLI_PARAM_SIZE_HINT]: raw.byteLength } });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new CheckpointCodecError(error.code === 'ERR_BUFFER_TOO_LARGE' ? 'encoded_size_limit' : 'compression_failed');
|
|
34
|
+
}
|
|
35
|
+
if (data.byteLength > limits.maxEncodedBytes)
|
|
36
|
+
throw new CheckpointCodecError('encoded_size_limit');
|
|
37
|
+
return { format: FORMAT, version: 1, encoding: 'br', decodedBytes: raw.byteLength, sha256: await hash(raw), data };
|
|
38
|
+
}
|
|
39
|
+
export async function decodeCheckpoint(envelope, overrides = {}) {
|
|
40
|
+
const limits = options(overrides);
|
|
41
|
+
if (!envelope || envelope.format !== FORMAT || envelope.version !== 1 || envelope.encoding !== 'br')
|
|
42
|
+
throw new CheckpointCodecError('unsupported_format');
|
|
43
|
+
if (!(envelope.data instanceof Uint8Array) || !Number.isSafeInteger(envelope.decodedBytes) || envelope.decodedBytes < 1 || typeof envelope.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(envelope.sha256))
|
|
44
|
+
throw new CheckpointCodecError('invalid_envelope');
|
|
45
|
+
if (envelope.data.byteLength > limits.maxEncodedBytes)
|
|
46
|
+
throw new CheckpointCodecError('encoded_size_limit');
|
|
47
|
+
if (envelope.decodedBytes > limits.maxDecodedBytes)
|
|
48
|
+
throw new CheckpointCodecError('decoded_size_limit');
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = await brotliDecompress(envelope.data, { maxOutputLength: Math.min(limits.maxDecodedBytes, envelope.decodedBytes) });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new CheckpointCodecError(error.code === 'ERR_BUFFER_TOO_LARGE' ? 'decoded_size_limit' : 'corrupt_payload');
|
|
55
|
+
}
|
|
56
|
+
if (raw.byteLength !== envelope.decodedBytes || await hash(raw) !== envelope.sha256)
|
|
57
|
+
throw new CheckpointCodecError('integrity_mismatch');
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new CheckpointCodecError('invalid_json_payload');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function assertActionRecoveryIdentity(value: any): {
|
|
2
|
+
protocol: number;
|
|
3
|
+
runtime: any;
|
|
4
|
+
definitions: any;
|
|
5
|
+
};
|
|
6
|
+
export function actionRecoveryIdentityMatches(current: any, stored: any): boolean;
|
|
7
|
+
/** Compute once from immutable loaded declarations; callers must not mutate them after activation. */
|
|
8
|
+
export function createActionRecoveryIdentity({ flows, tools }: {
|
|
9
|
+
flows: any;
|
|
10
|
+
tools: any;
|
|
11
|
+
}): Promise<Readonly<{
|
|
12
|
+
protocol: 1;
|
|
13
|
+
runtime: string;
|
|
14
|
+
definitions: string;
|
|
15
|
+
}>>;
|
|
16
|
+
//# sourceMappingURL=recovery-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-identity.d.ts","sourceRoot":"","sources":["../../src/host/recovery-identity.js"],"names":[],"mappings":"AAIA;;;;EAGC;AACD,kFAEC;AA4BD,sGAAsG;AACtG;;;;;;;IAMC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Immutable checkpoint compatibility. Browser and bundled Node hosts use the same stamped runtime. */
|
|
2
|
+
import { JSFE_RUNTIME_FINGERPRINT } from '../runtime-identity.js';
|
|
3
|
+
const hex = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
4
|
+
const runtimeHash = value => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
5
|
+
export function assertActionRecoveryIdentity(value) {
|
|
6
|
+
if (!value || value.protocol !== 1 || !runtimeHash(value.runtime) || !hex(value.definitions) || Object.keys(value).some(key => !['protocol', 'runtime', 'definitions'].includes(key)))
|
|
7
|
+
throw new Error('Invalid action recovery identity');
|
|
8
|
+
return { protocol: 1, runtime: value.runtime, definitions: value.definitions };
|
|
9
|
+
}
|
|
10
|
+
export function actionRecoveryIdentityMatches(current, stored) {
|
|
11
|
+
try {
|
|
12
|
+
const a = assertActionRecoveryIdentity(current), b = assertActionRecoveryIdentity(stored);
|
|
13
|
+
return a.runtime === b.runtime && a.definitions === b.definitions;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const canonical = value => {
|
|
20
|
+
const active = new Set();
|
|
21
|
+
const visit = value => {
|
|
22
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
23
|
+
return value;
|
|
24
|
+
if (typeof value === 'number') {
|
|
25
|
+
if (!Number.isFinite(value))
|
|
26
|
+
throw new Error('Recovery contracts require finite JSON values');
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== 'object' || active.has(value))
|
|
30
|
+
throw new Error('Recovery contracts require acyclic JSON declarations');
|
|
31
|
+
active.add(value);
|
|
32
|
+
let result;
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
result = value.map(item => item === undefined ? null : visit(item));
|
|
35
|
+
else {
|
|
36
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
37
|
+
throw new Error('Recovery contracts require plain JSON objects');
|
|
38
|
+
result = Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, visit(value[key])]));
|
|
39
|
+
}
|
|
40
|
+
active.delete(value);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
return JSON.stringify(visit(value));
|
|
44
|
+
};
|
|
45
|
+
const registry = (entries, name) => {
|
|
46
|
+
if (!Array.isArray(entries))
|
|
47
|
+
throw new Error('Recovery identity requires ' + name + ' registry');
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry || typeof entry.id !== 'string' || !entry.id || seen.has(entry.id))
|
|
51
|
+
throw new Error('Recovery identity requires unique ' + name + ' IDs');
|
|
52
|
+
seen.add(entry.id);
|
|
53
|
+
}
|
|
54
|
+
return [...entries].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
55
|
+
};
|
|
56
|
+
const select = (value, keys) => Object.fromEntries(keys.filter(key => value && value[key] !== undefined).map(key => [key, value[key]]));
|
|
57
|
+
const executionKeys = ['type', 'function', 'url', 'method', 'contentType', 'pathParams', 'queryParams', 'responseMapping', 'timeout', 'retries', 'acceptStatus'];
|
|
58
|
+
function toolContract(tool) {
|
|
59
|
+
const result = select(tool, ['id', 'name', 'description', 'schema', 'parameters', 'returns']);
|
|
60
|
+
if (tool.implementation) {
|
|
61
|
+
result.implementation = select(tool.implementation, executionKeys);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/** Compute once from immutable loaded declarations; callers must not mutate them after activation. */
|
|
66
|
+
export async function createActionRecoveryIdentity({ flows, tools }) {
|
|
67
|
+
if (!runtimeHash(JSFE_RUNTIME_FINGERPRINT))
|
|
68
|
+
throw new Error('Durable action recovery requires a verified stamped JSFE build');
|
|
69
|
+
const declarations = { flows: registry(flows, 'flow'), tools: registry(tools, 'tool').map(toolContract) };
|
|
70
|
+
const bytes = new TextEncoder().encode(canonical(declarations));
|
|
71
|
+
const definitions = Array.from(new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bytes)), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
72
|
+
return Object.freeze({ protocol: 1, runtime: JSFE_RUNTIME_FINGERPRINT, definitions });
|
|
73
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Call after changing or deleting host-owned state. Old overrides do not override a newer checkpoint. */
|
|
2
|
+
export function retainRecoveryState(session: any, paths: any): void;
|
|
3
|
+
/** Apply only invalidations newer than the checkpoint, preserving JSON deletion tombstones. */
|
|
4
|
+
export function applyRecoveryStatePolicy(restored: any, current: any): any;
|
|
5
|
+
//# sourceMappingURL=recovery-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-policy.d.ts","sourceRoot":"","sources":["../../src/host/recovery-policy.js"],"names":[],"mappings":"AAOA,0GAA0G;AAC1G,oEAYC;AACD,+FAA+F;AAC/F,2EAYC"}
|