@fusionkit/handoff 0.1.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/tools.js ADDED
@@ -0,0 +1,99 @@
1
+ import { hashCanonical } from "@fusionkit/protocol";
2
+ /**
3
+ * The journal's canonicalization contract: entries store the JSON
4
+ * projection of tool inputs/outputs (what JSON.stringify yields — Dates as
5
+ * ISO strings, undefined/functions/symbols dropped), and journal hashes
6
+ * are defined over that projection. Values JSON cannot represent at all
7
+ * (BigInt, circular structures) are recorded as their string form. This is
8
+ * deterministic for any given value, which is what continuation replay
9
+ * needs; it does not claim byte-fidelity with tool-native serialization.
10
+ */
11
+ function toJsonValue(value) {
12
+ if (value === undefined)
13
+ return null;
14
+ try {
15
+ return JSON.parse(JSON.stringify(value));
16
+ }
17
+ catch {
18
+ return String(value);
19
+ }
20
+ }
21
+ /** Flatten an error (with its cause chain) into a journalable string. */
22
+ function describeError(error) {
23
+ if (!(error instanceof Error))
24
+ return String(error);
25
+ const parts = [`${error.name}: ${error.message}`];
26
+ let cause = error.cause;
27
+ for (let depth = 0; cause !== undefined && depth < 5; depth++) {
28
+ parts.push(cause instanceof Error
29
+ ? `caused by ${cause.name}: ${cause.message}`
30
+ : `caused by ${String(cause)}`);
31
+ cause = cause instanceof Error ? cause.cause : undefined;
32
+ }
33
+ return parts.join(" — ");
34
+ }
35
+ /**
36
+ * Wrap a toolset so every invocation is journaled: raw input/output go to
37
+ * the journal (carried as content-addressed semantic state at the next
38
+ * checkpoint), and the observer receives hashes for the local trace.
39
+ * Everything else about each tool — description, schema, identity — is
40
+ * preserved, so the wrapped set drops into generateText unchanged.
41
+ */
42
+ export function wrapTools(toolset, nextSeq, observe) {
43
+ const wrapped = {};
44
+ for (const [name, original] of Object.entries(toolset)) {
45
+ // The journal is an observer, not a validator: input validation is the
46
+ // tool's own contract (AI SDK tools validate against their inputSchema
47
+ // before execute runs), and the journal faithfully records whatever was
48
+ // actually executed — including calls a tool later rejects.
49
+ const execute = original.execute;
50
+ if (typeof execute !== "function") {
51
+ wrapped[name] = original;
52
+ continue;
53
+ }
54
+ const callable = execute;
55
+ wrapped[name] = {
56
+ ...original,
57
+ execute: async (input, options) => {
58
+ const started = Date.now();
59
+ const ts = new Date(started).toISOString();
60
+ const inputJson = toJsonValue(input);
61
+ const inputHash = hashCanonical(inputJson);
62
+ try {
63
+ const output = await callable.call(original, input, options);
64
+ const outputJson = toJsonValue(output);
65
+ observe({
66
+ record: {
67
+ seq: nextSeq(),
68
+ ts,
69
+ toolName: name,
70
+ input: inputJson,
71
+ output: outputJson,
72
+ durationMs: Date.now() - started
73
+ },
74
+ inputHash,
75
+ outputHash: hashCanonical(outputJson),
76
+ ok: true
77
+ });
78
+ return output;
79
+ }
80
+ catch (error) {
81
+ observe({
82
+ record: {
83
+ seq: nextSeq(),
84
+ ts,
85
+ toolName: name,
86
+ input: inputJson,
87
+ error: describeError(error),
88
+ durationMs: Date.now() - started
89
+ },
90
+ inputHash,
91
+ ok: false
92
+ });
93
+ throw error;
94
+ }
95
+ }
96
+ };
97
+ }
98
+ return wrapped;
99
+ }
@@ -0,0 +1,6 @@
1
+ import type { HandoffTraceEvent } from "./handoff.js";
2
+ export declare class HandoffTraceLog {
3
+ private readonly events;
4
+ append(event: HandoffTraceEvent): void;
5
+ snapshot(): HandoffTraceEvent[];
6
+ }
@@ -0,0 +1,9 @@
1
+ export class HandoffTraceLog {
2
+ events = [];
3
+ append(event) {
4
+ this.events.push(event);
5
+ }
6
+ snapshot() {
7
+ return [...this.events];
8
+ }
9
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Typed continuation triggers. Deterministic and explainable: every trigger
3
+ * evaluates against observable context state (the tool journal, explicit
4
+ * requests, model routing decisions) — never against a model's opinion.
5
+ */
6
+ export type Trigger = {
7
+ kind: "trigger";
8
+ id: "user-requested";
9
+ } | {
10
+ kind: "trigger";
11
+ id: "tool-failed";
12
+ } | {
13
+ kind: "trigger";
14
+ id: "slow-tools";
15
+ thresholdMs: number;
16
+ } | {
17
+ kind: "trigger";
18
+ id: "model-escalated";
19
+ };
20
+ export declare const triggers: {
21
+ /** The user (or app) explicitly asked via h.requestContinuation(). */
22
+ userRequested(): Trigger;
23
+ /** Any journaled tool call failed. */
24
+ toolFailed(): Trigger;
25
+ /** Cumulative journaled tool time exceeded the threshold. */
26
+ slowTools(options: {
27
+ thresholdMs: number;
28
+ }): Trigger;
29
+ /** h.model escalated from the local model to the cloud model. */
30
+ modelEscalated(): Trigger;
31
+ };
32
+ /** Observable context state that triggers evaluate against. */
33
+ export type TriggerState = {
34
+ userRequested: boolean;
35
+ toolFailures: number;
36
+ totalToolDurationMs: number;
37
+ modelEscalations: number;
38
+ };
39
+ export type FiredTrigger = {
40
+ trigger: Trigger;
41
+ reason: string;
42
+ };
43
+ export declare function evaluateTriggers(list: Trigger[], state: TriggerState): FiredTrigger[];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Typed continuation triggers. Deterministic and explainable: every trigger
3
+ * evaluates against observable context state (the tool journal, explicit
4
+ * requests, model routing decisions) — never against a model's opinion.
5
+ */
6
+ export const triggers = {
7
+ /** The user (or app) explicitly asked via h.requestContinuation(). */
8
+ userRequested() {
9
+ return { kind: "trigger", id: "user-requested" };
10
+ },
11
+ /** Any journaled tool call failed. */
12
+ toolFailed() {
13
+ return { kind: "trigger", id: "tool-failed" };
14
+ },
15
+ /** Cumulative journaled tool time exceeded the threshold. */
16
+ slowTools(options) {
17
+ return { kind: "trigger", id: "slow-tools", thresholdMs: options.thresholdMs };
18
+ },
19
+ /** h.model escalated from the local model to the cloud model. */
20
+ modelEscalated() {
21
+ return { kind: "trigger", id: "model-escalated" };
22
+ }
23
+ };
24
+ export function evaluateTriggers(list, state) {
25
+ const fired = [];
26
+ for (const trigger of list) {
27
+ switch (trigger.id) {
28
+ case "user-requested":
29
+ if (state.userRequested) {
30
+ fired.push({ trigger, reason: "continuation explicitly requested" });
31
+ }
32
+ break;
33
+ case "tool-failed":
34
+ if (state.toolFailures > 0) {
35
+ fired.push({
36
+ trigger,
37
+ reason: `${state.toolFailures} journaled tool call(s) failed`
38
+ });
39
+ }
40
+ break;
41
+ case "slow-tools":
42
+ if (state.totalToolDurationMs > trigger.thresholdMs) {
43
+ fired.push({
44
+ trigger,
45
+ reason: `tools consumed ${state.totalToolDurationMs}ms locally (threshold ${trigger.thresholdMs}ms)`
46
+ });
47
+ }
48
+ break;
49
+ case "model-escalated":
50
+ if (state.modelEscalations > 0) {
51
+ fired.push({
52
+ trigger,
53
+ reason: `the model escalated ${state.modelEscalations} time(s)`
54
+ });
55
+ }
56
+ break;
57
+ default: {
58
+ const exhausted = trigger;
59
+ throw new Error(`unreachable trigger: ${String(exhausted)}`);
60
+ }
61
+ }
62
+ }
63
+ return fired;
64
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@fusionkit/handoff",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/velum-labs/handoffkit.git",
8
+ "directory": "packages/handoff"
9
+ },
10
+ "description": "Handoff SDK: continuation-first developer surface (checkpoint, continueIn, parallel, review, pull) built entirely on Warrant primitives.",
11
+ "license": "UNLICENSED",
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "registry": "https://registry.npmjs.org",
24
+ "access": "public",
25
+ "provenance": true
26
+ },
27
+ "dependencies": {
28
+ "@fusionkit/protocol": "0.1.0",
29
+ "@fusionkit/sdk": "0.1.0",
30
+ "@fusionkit/workspace": "0.1.0"
31
+ }
32
+ }