@chrok/dsh-braid 0.0.0-stage → 0.3.2

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.
@@ -0,0 +1,29 @@
1
+ /** One descriptor table shared by Host registration and Client Remote mounting. */
2
+ export const PANEL_PACKAGE = "@chrok/dsh-braid";
3
+ function parseRequest(value, method) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value))
5
+ throw new Error("Invalid Braid panel request");
6
+ const r = value;
7
+ const id = (key) => typeof r[key] === "string" && r[key].length > 0 && r[key].length <= 1024;
8
+ if (!id("sessionId") || (r.jobId !== undefined && !id("jobId")))
9
+ throw new Error("Invalid session or job ID");
10
+ if (r.createdBefore !== undefined && (!Number.isSafeInteger(r.createdBefore) || Number(r.createdBefore) < 0))
11
+ throw new Error("Invalid call timestamp");
12
+ if (method !== "watch" && !id("jobId"))
13
+ throw new Error("A job ID is required");
14
+ if (method === "control" && (!["cancel", "resume"].includes(String(r.action)) || !Number.isSafeInteger(r.revision) || Number(r.revision) < 0 ||
15
+ !Array.isArray(r.executionIds) || r.executionIds.length > 10000 || r.executionIds.some(v => typeof v !== "string" || !v || v.length > 1024)))
16
+ throw new Error("Invalid Braid control request");
17
+ if (method === "detail" && (!id("executionId") || !Number.isSafeInteger(r.offset) || Number(r.offset) < 0))
18
+ throw new Error("Invalid execution or offset");
19
+ return value;
20
+ }
21
+ export const panelDescriptors = ["watch", "control", "detail"].map(method => ({
22
+ id: `${PANEL_PACKAGE}#braidPanel/${method}`, service: "braidPanel", namespace: "braidPanel", method,
23
+ invocation: { kind: "direct" },
24
+ parameters: [{ name: "request", wire: "request", source: "json", codec: {
25
+ mode: "strict", typeSymbol: `${PANEL_PACKAGE}/${method}Request`, create: () => ({ parse: value => parseRequest(value, method) }),
26
+ } }],
27
+ result: { mode: "src-json" },
28
+ ...(method === "watch" ? { mode: "stream", cancellation: { parameter: "signal" } } : {}),
29
+ }));
@@ -0,0 +1,100 @@
1
+ /** Browser-safe, bounded projections. Prompts and provider messages never enter the panel stream. */
2
+ export interface PanelRequest {
3
+ sessionId: string;
4
+ jobId?: string;
5
+ createdBefore?: number;
6
+ }
7
+ export interface ControlRequest extends PanelRequest {
8
+ jobId: string;
9
+ action: "cancel" | "resume";
10
+ revision: number;
11
+ executionIds: string[];
12
+ }
13
+ export interface DetailRequest extends PanelRequest {
14
+ jobId: string;
15
+ executionId: string;
16
+ offset: number;
17
+ }
18
+ export interface PanelRow {
19
+ jobId: string;
20
+ handle: string;
21
+ goal: string;
22
+ status: string;
23
+ createdAt: number;
24
+ }
25
+ export interface PanelExecution {
26
+ id: string;
27
+ executionId: string;
28
+ status: string;
29
+ type: string;
30
+ revision: number;
31
+ iteration?: number;
32
+ loopId?: string;
33
+ latencyMs?: number;
34
+ progress?: {
35
+ contextTokens: number;
36
+ contextWindow?: number;
37
+ contextSource: string;
38
+ toolCalls: number;
39
+ toolRounds: number;
40
+ phase: string;
41
+ };
42
+ }
43
+ export interface PanelJob extends PanelRow {
44
+ revision: number;
45
+ phase: string;
46
+ error?: string;
47
+ nodes: {
48
+ id: string;
49
+ type: string;
50
+ status: string;
51
+ }[];
52
+ edges: {
53
+ from: string;
54
+ to: string;
55
+ choice?: string;
56
+ feedback?: string;
57
+ executionId?: string;
58
+ }[];
59
+ loops: {
60
+ id: string;
61
+ entry: string;
62
+ maxIterations: number;
63
+ }[];
64
+ executions: PanelExecution[];
65
+ pausedExecutionIds: string[];
66
+ usage: {
67
+ inputTokens: number;
68
+ outputTokens: number;
69
+ cacheReadTokens: number;
70
+ cacheWriteTokens: number;
71
+ };
72
+ events: {
73
+ sequence: number;
74
+ timestamp: number;
75
+ type: string;
76
+ executionId?: string;
77
+ nodeId?: string;
78
+ message: string;
79
+ }[];
80
+ }
81
+ export interface PanelFrame {
82
+ rows: PanelRow[];
83
+ job: PanelJob | null;
84
+ }
85
+ export interface PanelDetail {
86
+ executionId: string;
87
+ status: string;
88
+ output: string;
89
+ offset: number;
90
+ next: number;
91
+ total: number;
92
+ decision?: string;
93
+ error?: string;
94
+ workspace?: string;
95
+ }
96
+ export interface PanelApi {
97
+ watch(request: PanelRequest, signal: AbortSignal): AsyncIterable<PanelFrame>;
98
+ control(request: ControlRequest): Promise<PanelFrame>;
99
+ detail(request: DetailRequest): Promise<PanelDetail>;
100
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
+ import type { BraidJobs, JobSnapshot } from "./jobs.js";
4
+ import type { ControlRequest, DetailRequest, PanelDetail, PanelFrame, PanelJob, PanelRequest } from "./panel-types.js";
5
+ export declare function panelJob(job: JobSnapshot): PanelJob;
6
+ /** Session managers are borrowed; viewing a panel never creates an Agent or starts model work. */
7
+ export declare class BraidPanel extends TypertRemoteService {
8
+ private readonly find;
9
+ private readonly subscribe;
10
+ constructor(ctx: Context, find: (sessionId: string) => BraidJobs | undefined, subscribe: (listener: () => void) => () => void);
11
+ snapshot(request: PanelRequest): PanelFrame;
12
+ watch(request: PanelRequest, signal: AbortSignal): AsyncIterable<PanelFrame>;
13
+ control(request: ControlRequest): PanelFrame;
14
+ detail(request: DetailRequest): PanelDetail;
15
+ private failure;
16
+ }
package/dist/panel.js ADDED
@@ -0,0 +1,169 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
36
+ import { PANEL_PACKAGE, panelDescriptors } from "./panel-contract.js";
37
+ const clip = (text, limit = 2000) => text.length > limit ? text.slice(0, limit) + "…" : text;
38
+ export function panelJob(job) {
39
+ const state = job.execution;
40
+ return {
41
+ jobId: job.jobId, handle: job.handle, goal: clip(job.goal), status: job.status, createdAt: job.createdAt,
42
+ revision: state.revision, phase: state.status,
43
+ ...(job.error || state.error ? { error: clip(job.error ?? state.error.message) } : {}),
44
+ nodes: state.graph.nodes.map(node => ({ id: node.id, type: node.type, status: state.nodes[node.id]?.status ?? "pending" })),
45
+ edges: state.graph.edges.map(edge => ({ ...edge })), loops: [...state.graph.loops ?? []],
46
+ executions: Object.values(state.executions).map(e => ({
47
+ id: e.id, executionId: e.executionId, status: e.status, type: e.node.type, revision: e.revision,
48
+ ...(e.iteration === undefined ? {} : { iteration: e.iteration }),
49
+ ...(e.loopId === undefined ? {} : { loopId: e.loopId }),
50
+ ...(e.latencyMs === undefined ? {} : { latencyMs: e.latencyMs }),
51
+ ...(job.progress[e.executionId] ? { progress: job.progress[e.executionId] } : {}),
52
+ })),
53
+ pausedExecutionIds: [...state.pausedExecutionIds],
54
+ usage: { inputTokens: job.usage.inputTokens ?? 0, outputTokens: job.usage.outputTokens ?? 0,
55
+ cacheReadTokens: job.usage.cacheReadTokens ?? 0, cacheWriteTokens: job.usage.cacheWriteTokens ?? 0 },
56
+ events: job.events.map(e => ({ sequence: e.sequence, timestamp: e.timestamp, type: e.type,
57
+ ...(e.executionId ? { executionId: e.executionId } : {}), ...("nodeId" in e ? { nodeId: e.nodeId } : {}),
58
+ message: clip("error" in e ? e.error.message : e.type === "handoff" ? `${e.from} → ${e.to}` : e.type === "edge_created" ? `${e.from} → ${e.to}` : e.type.replaceAll("_", " "), 500),
59
+ })),
60
+ };
61
+ }
62
+ /** Session managers are borrowed; viewing a panel never creates an Agent or starts model work. */
63
+ let BraidPanel = (() => {
64
+ let _classSuper = TypertRemoteService;
65
+ let _instanceExtraInitializers = [];
66
+ let _watch_decorators;
67
+ let _control_decorators;
68
+ let _detail_decorators;
69
+ return class BraidPanel extends _classSuper {
70
+ static {
71
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
72
+ _watch_decorators = [Remote({ mode: "stream" })];
73
+ _control_decorators = [Remote];
74
+ _detail_decorators = [Remote];
75
+ __esDecorate(this, null, _watch_decorators, { kind: "method", name: "watch", static: false, private: false, access: { has: obj => "watch" in obj, get: obj => obj.watch }, metadata: _metadata }, null, _instanceExtraInitializers);
76
+ __esDecorate(this, null, _control_decorators, { kind: "method", name: "control", static: false, private: false, access: { has: obj => "control" in obj, get: obj => obj.control }, metadata: _metadata }, null, _instanceExtraInitializers);
77
+ __esDecorate(this, null, _detail_decorators, { kind: "method", name: "detail", static: false, private: false, access: { has: obj => "detail" in obj, get: obj => obj.detail }, metadata: _metadata }, null, _instanceExtraInitializers);
78
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
79
+ }
80
+ find = __runInitializers(this, _instanceExtraInitializers);
81
+ subscribe;
82
+ constructor(ctx, find, subscribe) {
83
+ super(ctx, "braidPanel");
84
+ this.find = find;
85
+ this.subscribe = subscribe;
86
+ ctx.typert.register({ package: PANEL_PACKAGE, face: "host", schemas: [], model: { services: [], events: [], objects: [] }, invocations: panelDescriptors });
87
+ }
88
+ snapshot(request) {
89
+ const jobs = this.find(request.sessionId);
90
+ const rows = jobs?.list().map(row => ({ ...row, goal: clip(row.goal) })) ?? [];
91
+ // Old tool cards can carry short handles. Fence them by the call's timestamp
92
+ // so a Host/plugin restart cannot redirect an old card to a reused handle.
93
+ const selected = request.jobId ? rows.find(row => (row.jobId === request.jobId || row.handle === request.jobId)
94
+ && (request.createdBefore === undefined || row.createdAt <= request.createdBefore)) : rows[0];
95
+ if (request.jobId && !selected)
96
+ throw new Error("Braid job is no longer available in this session");
97
+ const id = selected?.jobId;
98
+ return { rows, job: id && jobs ? panelJob(jobs.get(id)) : null };
99
+ }
100
+ async *watch(request, signal) {
101
+ let dirty = true, wake;
102
+ const changed = () => { dirty = true; wake?.(); };
103
+ const off = this.subscribe(changed);
104
+ signal.addEventListener("abort", changed, { once: true });
105
+ try {
106
+ while (!signal.aborted) {
107
+ if (!dirty)
108
+ await new Promise(resolve => { wake = resolve; });
109
+ wake = undefined;
110
+ if (signal.aborted)
111
+ break;
112
+ dirty = false;
113
+ try {
114
+ yield this.snapshot(request);
115
+ }
116
+ catch (error) {
117
+ throw this.failure(request, error);
118
+ }
119
+ // Coalesce token bursts; cancellation also wakes this delay.
120
+ await new Promise(resolve => {
121
+ const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); };
122
+ const timer = setTimeout(done, 150);
123
+ signal.addEventListener("abort", done, { once: true });
124
+ if (signal.aborted)
125
+ done();
126
+ });
127
+ }
128
+ }
129
+ finally {
130
+ off();
131
+ signal.removeEventListener("abort", changed);
132
+ }
133
+ }
134
+ control(request) {
135
+ try {
136
+ this.snapshot(request);
137
+ const jobs = this.find(request.sessionId);
138
+ if (request.action === "resume")
139
+ jobs.resume(request.jobId, request.executionIds, request.revision);
140
+ else
141
+ jobs.cancel(request.jobId);
142
+ return this.snapshot(request);
143
+ }
144
+ catch (error) {
145
+ throw this.failure(request, error);
146
+ }
147
+ }
148
+ detail(request) {
149
+ try {
150
+ this.snapshot(request);
151
+ const node = this.find(request.sessionId).getNode(request.jobId, undefined, request.executionId);
152
+ const output = node.output ?? "";
153
+ const offset = Math.min(request.offset, output.length), next = Math.min(offset + 32768, output.length);
154
+ return { executionId: request.executionId, status: node.status, output: output.slice(offset, next), offset, next, total: output.length,
155
+ ...(node.decision === undefined ? {} : { decision: clip(node.decision) }),
156
+ ...(node.error ? { error: clip(node.error.message) } : {}),
157
+ ...(node.workspace ? { workspace: clip(JSON.stringify(node.workspace, null, 2), 8000) } : {}),
158
+ };
159
+ }
160
+ catch (error) {
161
+ throw this.failure(request, error);
162
+ }
163
+ }
164
+ failure(request, error) {
165
+ return new RemoteError("braid/panel", error instanceof Error ? error.message : String(error), { sessionId: request.sessionId });
166
+ }
167
+ };
168
+ })();
169
+ export { BraidPanel };
@@ -0,0 +1,27 @@
1
+ import { type LlmRuntime, type TokenUsage } from "@deepseek-ai/dsh-llm";
2
+ import { type ModelRunner } from "@chrok/braid";
3
+ export interface NodeProgress {
4
+ nodeId: string;
5
+ executionId: string;
6
+ contextTokens: number;
7
+ contextWindow?: number;
8
+ contextSource: "reported" | "estimate";
9
+ toolCalls: number;
10
+ toolRounds: number;
11
+ phase: "model" | "tool";
12
+ }
13
+ export interface RunnerOptions {
14
+ cwd: string;
15
+ maxToolRounds?: number;
16
+ maxToolCalls?: number;
17
+ onUsage?: (usage: TokenUsage) => void;
18
+ onProgress?: (progress: NodeProgress) => void;
19
+ }
20
+ export declare function validateBudgets(options: {
21
+ maxToolRounds?: number | undefined;
22
+ maxToolCalls?: number | undefined;
23
+ }): void;
24
+ /** Fresh, private model contexts. DSH owns provider routing and credentials. */
25
+ export type DshLlm = Pick<LlmRuntime, "stream"> & Partial<Pick<LlmRuntime, "resolveModelInfo">>;
26
+ export declare function createDshRunner(llm: DshLlm, options: RunnerOptions): ModelRunner;
27
+ export declare function sumUsage(reports: readonly TokenUsage[]): TokenUsage;
package/dist/runner.js ADDED
@@ -0,0 +1,176 @@
1
+ import { resolve } from "node:path";
2
+ import { BlockAssembler, createToolResultMessage, } from "@deepseek-ai/dsh-llm";
3
+ import { formatBudgetReminder, gitToolDefinition, finishMergeToolDefinition, mergeInstructions, parseGitToolArguments, parseFinishMergeArguments, } from "@chrok/braid";
4
+ import { createWorkerTools } from "./worker-tools.js";
5
+ export function validateBudgets(options) {
6
+ for (const [key, value] of Object.entries(options)) {
7
+ if (value !== undefined && value !== Infinity && (!Number.isSafeInteger(value) || value < 1))
8
+ throw new Error(`${key} must be a positive safe integer or Infinity`);
9
+ }
10
+ }
11
+ export function createDshRunner(llm, options) {
12
+ validateBudgets({ maxToolRounds: options.maxToolRounds, maxToolCalls: options.maxToolCalls });
13
+ return async (request) => {
14
+ const route = request.model;
15
+ const slash = route?.indexOf("/") ?? -1;
16
+ if (!route || slash < 1 || slash === route.length - 1)
17
+ throw new Error("Choose a DSH provider/model, or set node.model to an exact provider/model-id");
18
+ const provider = route.slice(0, slash), model = route.slice(slash + 1);
19
+ const modelInfo = await llm.resolveModelInfo?.(provider, model, request.signal);
20
+ const contextWindow = modelInfo?.context?.contextWindow;
21
+ request.signal.throwIfAborted();
22
+ const workspace = request.workspace ?? {
23
+ nodeId: request.node.id, mode: "read-only",
24
+ workingDirectory: resolve(options.cwd), state: "ready",
25
+ };
26
+ const files = await createWorkerTools(request, workspace);
27
+ const tools = files.map(({ name, description, parameters }) => ({ name, description, parameters: JSON.parse(JSON.stringify(parameters)) }));
28
+ const integrating = request.node.type === "merge" || request.node.type === "integrate";
29
+ if (request.git)
30
+ tools.push(gitToolDefinition(integrating));
31
+ if (request.merge)
32
+ tools.push(finishMergeToolDefinition(request.merge.sources.map(source => source.executionId)));
33
+ if (request.node.type === "decision")
34
+ tools.push({
35
+ name: "decide", description: "Select exactly one declared choice before answering.",
36
+ parameters: { type: "object", properties: { choice: { type: "string", enum: request.node.choices } }, required: ["choice"], additionalProperties: false },
37
+ });
38
+ const messages = [{ role: "user", content: [{ type: "text", text: JSON.stringify({
39
+ goal: request.goal, nodeId: request.node.id, execution: request.execution,
40
+ prompt: request.node.prompt, predecessors: request.predecessors, workspace,
41
+ ...(request.merge ? { mergeSources: request.merge.sources, sourceCheckoutStatus: request.merge.sourceStatus } : {}),
42
+ }) }] }];
43
+ const system = "You are an isolated Braid worker. Complete only the assigned node. " +
44
+ "Use read, ls, grep, and find to inspect files. Only writable Git workspaces have write, edit, and shell tools. " +
45
+ "Use your assigned workingDirectory. Never write outside it; integrate alone may edit sourceRoot. " +
46
+ "Shell tools have host permissions, not a sandbox. Do not change shared Git refs, branches, configuration, hooks, or worktree registrations. " +
47
+ "Do not launch recursive agents, daemons, or background servers. Use execution-specific temporary resources. " +
48
+ "Checkpoints exclude ignored new dependencies, caches, and build outputs. Run tests for your changes. " +
49
+ "Parent transcript, skills, extension tools, and MCP tools are not inherited. " +
50
+ mergeInstructions(request) +
51
+ (request.node.type === "decision" ? "Call decide exactly once, then answer. " : "") +
52
+ "Return a nonempty natural-language result.";
53
+ let toolRounds = 0, toolCalls = 0, decided = false;
54
+ let contextTokens = 0;
55
+ let contextSource = "estimate";
56
+ const reports = [];
57
+ const output = [];
58
+ const progress = (phase) => {
59
+ try {
60
+ options.onProgress?.({ nodeId: request.node.id, executionId: request.execution.executionId, contextTokens, ...(contextWindow === undefined ? {} : { contextWindow }), contextSource, toolCalls, toolRounds, phase });
61
+ }
62
+ catch { /* Observers cannot fail a worker. */ }
63
+ };
64
+ while (true) {
65
+ request.signal.throwIfAborted();
66
+ const budgets = [
67
+ ...(options.maxToolRounds === undefined ? [] : [`Tool round budget: ${toolRounds}/${options.maxToolRounds} used.`]),
68
+ ...(options.maxToolCalls === undefined ? [] : [`Tool call budget: ${toolCalls}/${options.maxToolCalls} used; rejected requests count.`]),
69
+ "Reserve budget for decide or finish_merge. At zero remaining, return your final answer without further tools.",
70
+ ];
71
+ const activeTools = tools.filter(tool => !decided || tool.name !== "decide");
72
+ const prompt = system + formatBudgetReminder(request, budgets);
73
+ contextTokens = Math.ceil(JSON.stringify({ messages, system: prompt, tools: activeTools }).length / 4);
74
+ contextSource = "estimate";
75
+ progress("model");
76
+ const assembler = new BlockAssembler();
77
+ let finished = false;
78
+ try {
79
+ for await (const chunk of llm.stream({ provider, model, messages, system: prompt, tools: activeTools, signal: request.signal })) {
80
+ request.signal.throwIfAborted();
81
+ assembler.push(chunk);
82
+ if (chunk.type === "finish")
83
+ finished = true;
84
+ }
85
+ }
86
+ finally {
87
+ if (assembler.usage) {
88
+ const usage = structuredClone(assembler.usage);
89
+ reports.push(usage);
90
+ options.onUsage?.(usage);
91
+ contextTokens = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
92
+ contextSource = "reported";
93
+ progress("model");
94
+ }
95
+ }
96
+ request.signal.throwIfAborted();
97
+ const finish = assembler.finish;
98
+ if (!finished)
99
+ throw new Error("DSH model stream ended without a finish event");
100
+ if (finish.kind !== "stop" && finish.kind !== "tool-calls")
101
+ throw new Error("failure" in finish ? finish.failure.message : `DSH model ended with ${finish.kind}`);
102
+ const response = assembler.message({ provider, model, ...(assembler.replayState ? { replayState: assembler.replayState } : {}) });
103
+ output.push(...response.content.flatMap(block => block.type === "text" ? [block.text] : []));
104
+ const calls = response.content.filter(block => block.type === "tool-call");
105
+ if (!calls.length) {
106
+ if (finish.kind === "tool-calls")
107
+ throw new Error("DSH requested tool use without tool calls");
108
+ break;
109
+ }
110
+ if (finish.kind !== "tool-calls")
111
+ throw new Error("DSH returned tool calls with an invalid finish reason");
112
+ toolRounds++;
113
+ toolCalls += calls.length;
114
+ progress("tool");
115
+ if (toolRounds > (options.maxToolRounds ?? Infinity) || toolCalls > (options.maxToolCalls ?? Infinity))
116
+ throw new Error("Braid node exceeded its tool budget");
117
+ messages.push(response);
118
+ for (const call of calls) {
119
+ request.signal.throwIfAborted();
120
+ let text, isError = false;
121
+ try {
122
+ const args = JSON.parse(call.arguments);
123
+ if (call.name === "decide" && request.decide) {
124
+ if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).length !== 1 || !("choice" in args) || typeof args.choice !== "string")
125
+ throw new Error("decide requires exactly one string argument: choice");
126
+ request.decide(args.choice);
127
+ decided = true;
128
+ text = JSON.stringify({ choice: args.choice });
129
+ }
130
+ else if (call.name === "git" && request.git) {
131
+ const parsed = parseGitToolArguments(args, integrating);
132
+ const result = await request.git(parsed.args, parsed.input);
133
+ text = JSON.stringify(result);
134
+ isError = result.exitCode !== 0;
135
+ }
136
+ else if (call.name === "finish_merge" && request.merge) {
137
+ await request.merge.finish(parseFinishMergeArguments(args, request.merge.sources.map(source => source.executionId)));
138
+ text = "Merge dispositions recorded. Return your final answer.";
139
+ }
140
+ else {
141
+ const tool = files.find(tool => tool.name === call.name);
142
+ if (!tool)
143
+ throw new Error(`Unavailable tool '${call.name}'. Available: ${activeTools.map(tool => tool.name).join(", ")}`);
144
+ const invoke = () => tool.execute(args);
145
+ text = tool.writes && request.withWorkspaceWrite ? await request.withWorkspaceWrite(invoke) : await invoke();
146
+ }
147
+ }
148
+ catch (error) {
149
+ request.signal.throwIfAborted();
150
+ // Invalid or duplicate decisions are terminal, matching core's exactly-once contract.
151
+ if (call.name === "decide" && request.decide)
152
+ throw error;
153
+ isError = true;
154
+ text = error instanceof Error ? error.message : String(error);
155
+ }
156
+ messages.push(createToolResultMessage({ callId: call.id, content: [{ type: "text", text }], isError }));
157
+ }
158
+ }
159
+ const text = output.filter(part => part.trim()).join("\n\n");
160
+ if (!text.trim())
161
+ throw new Error("DSH model returned no textual output");
162
+ const usage = sumUsage(reports);
163
+ return { output: text, model: route, usage: {
164
+ inputTokens: usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0), outputTokens: usage.outputTokens,
165
+ } };
166
+ };
167
+ }
168
+ export function sumUsage(reports) {
169
+ const total = { inputTokens: 0, outputTokens: 0 };
170
+ for (const report of reports)
171
+ for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"])
172
+ if (report[key] !== undefined)
173
+ total[key] = (total[key] ?? 0) + report[key];
174
+ total.totalTokens = total.inputTokens + total.outputTokens + (total.cacheReadTokens ?? 0) + (total.cacheWriteTokens ?? 0);
175
+ return total;
176
+ }
@@ -0,0 +1,64 @@
1
+ export declare const nodeParameters: import("@sinclair/typebox").TObject<{
2
+ id: import("@sinclair/typebox").TString;
3
+ type: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>;
4
+ prompt: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
5
+ template: import("@sinclair/typebox").TString;
6
+ variables: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>;
7
+ }>]>>;
8
+ choices: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
9
+ workspace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>>;
10
+ model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ notifyOnCompletion: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
12
+ pauseAfter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
13
+ requireSuccess: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
14
+ }>;
15
+ export declare const updateEdgeParameters: import("@sinclair/typebox").TObject<{
16
+ executionId: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
17
+ from: import("@sinclair/typebox").TString;
18
+ to: import("@sinclair/typebox").TString;
19
+ choice: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
20
+ feedback: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
21
+ }>;
22
+ export declare const loopParameters: import("@sinclair/typebox").TObject<{
23
+ id: import("@sinclair/typebox").TString;
24
+ entry: import("@sinclair/typebox").TString;
25
+ maxIterations: import("@sinclair/typebox").TInteger;
26
+ }>;
27
+ export declare const braidParameters: import("@sinclair/typebox").TObject<{
28
+ goal: import("@sinclair/typebox").TString;
29
+ nodes: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
30
+ id: import("@sinclair/typebox").TString;
31
+ type: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>;
32
+ prompt: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
33
+ template: import("@sinclair/typebox").TString;
34
+ variables: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>;
35
+ }>]>>;
36
+ choices: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
37
+ workspace: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>>;
38
+ model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
39
+ notifyOnCompletion: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
40
+ pauseAfter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
41
+ requireSuccess: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
42
+ }>>;
43
+ edges: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
44
+ from: import("@sinclair/typebox").TString;
45
+ to: import("@sinclair/typebox").TString;
46
+ choice: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
47
+ feedback: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
48
+ }>>;
49
+ promptTemplates: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
50
+ loops: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
51
+ id: import("@sinclair/typebox").TString;
52
+ entry: import("@sinclair/typebox").TString;
53
+ maxIterations: import("@sinclair/typebox").TInteger;
54
+ }>>>;
55
+ options: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
56
+ maxConcurrency: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
57
+ maxExecutions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
58
+ nodeTimeoutMs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
59
+ graphTimeoutMs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
60
+ maxToolRounds: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
61
+ maxToolCalls: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
62
+ }>>;
63
+ }>;
64
+ export declare const BRAID_USAGE_GUIDANCE: string;