@botbotgo/agent-harness 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +22 -0
- package/dist/api.js +48 -0
- package/dist/config/direct.yaml +48 -0
- package/dist/config/embedding-model.yaml +30 -0
- package/dist/config/model.yaml +44 -0
- package/dist/config/orchestra.yaml +92 -0
- package/dist/config/runtime.yaml +47 -0
- package/dist/config/vector-store.yaml +26 -0
- package/dist/contracts/types.d.ts +359 -0
- package/dist/contracts/types.js +1 -0
- package/dist/extensions.d.ts +16 -0
- package/dist/extensions.js +251 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/persistence/file-store.d.ts +39 -0
- package/dist/persistence/file-store.js +282 -0
- package/dist/presentation.d.ts +4 -0
- package/dist/presentation.js +175 -0
- package/dist/runtime/agent-runtime-adapter.d.ts +33 -0
- package/dist/runtime/agent-runtime-adapter.js +445 -0
- package/dist/runtime/event-bus.d.ts +6 -0
- package/dist/runtime/event-bus.js +15 -0
- package/dist/runtime/file-checkpoint-saver.d.ts +20 -0
- package/dist/runtime/file-checkpoint-saver.js +91 -0
- package/dist/runtime/harness.d.ts +57 -0
- package/dist/runtime/harness.js +696 -0
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +10 -0
- package/dist/runtime/inventory.d.ts +25 -0
- package/dist/runtime/inventory.js +62 -0
- package/dist/runtime/parsing/index.d.ts +2 -0
- package/dist/runtime/parsing/index.js +2 -0
- package/dist/runtime/parsing/output-parsing.d.ts +12 -0
- package/dist/runtime/parsing/output-parsing.js +424 -0
- package/dist/runtime/parsing/stream-event-parsing.d.ts +27 -0
- package/dist/runtime/parsing/stream-event-parsing.js +161 -0
- package/dist/runtime/policy-engine.d.ts +9 -0
- package/dist/runtime/policy-engine.js +23 -0
- package/dist/runtime/store.d.ts +50 -0
- package/dist/runtime/store.js +118 -0
- package/dist/runtime/support/embedding-models.d.ts +4 -0
- package/dist/runtime/support/embedding-models.js +33 -0
- package/dist/runtime/support/harness-support.d.ts +27 -0
- package/dist/runtime/support/harness-support.js +116 -0
- package/dist/runtime/support/index.d.ts +4 -0
- package/dist/runtime/support/index.js +4 -0
- package/dist/runtime/support/llamaindex.d.ts +24 -0
- package/dist/runtime/support/llamaindex.js +108 -0
- package/dist/runtime/support/runtime-factories.d.ts +3 -0
- package/dist/runtime/support/runtime-factories.js +39 -0
- package/dist/runtime/support/skill-metadata.d.ts +1 -0
- package/dist/runtime/support/skill-metadata.js +34 -0
- package/dist/runtime/support/vector-stores.d.ts +7 -0
- package/dist/runtime/support/vector-stores.js +130 -0
- package/dist/runtime/thread-memory-sync.d.ts +14 -0
- package/dist/runtime/thread-memory-sync.js +88 -0
- package/dist/runtime/tool-hitl.d.ts +5 -0
- package/dist/runtime/tool-hitl.js +108 -0
- package/dist/utils/fs.d.ts +6 -0
- package/dist/utils/fs.js +39 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.js +8 -0
- package/dist/vendor/builtins.d.ts +23 -0
- package/dist/vendor/builtins.js +103 -0
- package/dist/vendor/sources.d.ts +12 -0
- package/dist/vendor/sources.js +115 -0
- package/dist/workspace/agent-binding-compiler.d.ts +4 -0
- package/dist/workspace/agent-binding-compiler.js +181 -0
- package/dist/workspace/compile.d.ts +2 -0
- package/dist/workspace/compile.js +107 -0
- package/dist/workspace/index.d.ts +6 -0
- package/dist/workspace/index.js +6 -0
- package/dist/workspace/object-loader.d.ts +16 -0
- package/dist/workspace/object-loader.js +405 -0
- package/dist/workspace/resource-compilers.d.ts +13 -0
- package/dist/workspace/resource-compilers.js +182 -0
- package/dist/workspace/support/discovery.d.ts +5 -0
- package/dist/workspace/support/discovery.js +108 -0
- package/dist/workspace/support/index.d.ts +2 -0
- package/dist/workspace/support/index.js +2 -0
- package/dist/workspace/support/source-collectors.d.ts +3 -0
- package/dist/workspace/support/source-collectors.js +30 -0
- package/dist/workspace/support/workspace-ref-utils.d.ts +8 -0
- package/dist/workspace/support/workspace-ref-utils.js +50 -0
- package/dist/workspace/validate.d.ts +3 -0
- package/dist/workspace/validate.js +65 -0
- package/package.json +32 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { Command, MemorySaver } from "@langchain/langgraph";
|
|
2
|
+
import { createDeepAgent } from "deepagents";
|
|
3
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
4
|
+
import { ChatGoogle } from "@langchain/google";
|
|
5
|
+
import { ChatOllama } from "@langchain/ollama";
|
|
6
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
7
|
+
import { createAgent, humanInTheLoopMiddleware, initChatModel } from "langchain";
|
|
8
|
+
import { extractReasoningText, extractToolFallbackContext, extractVisibleOutput, isLikelyToolArgsObject, isToolCallParseFailure, STRICT_TOOL_JSON_INSTRUCTION, sanitizeVisibleText, tryParseJson, wrapResolvedModel, } from "./parsing/output-parsing.js";
|
|
9
|
+
import { extractAgentStep, extractInterruptPayload, extractReasoningStreamOutput, extractTerminalStreamOutput, extractToolResult, normalizeTerminalOutputKey, readStreamDelta, } from "./parsing/stream-event-parsing.js";
|
|
10
|
+
import { wrapToolForExecution } from "./tool-hitl.js";
|
|
11
|
+
const AGENT_INTERRUPT_SENTINEL_PREFIX = "__agent_harness_interrupt__:";
|
|
12
|
+
function asObject(value) {
|
|
13
|
+
return typeof value === "object" && value ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
function normalizeOpenAICompatibleInit(init) {
|
|
16
|
+
const normalized = { ...init };
|
|
17
|
+
const configuration = asObject(init.configuration) ?? {};
|
|
18
|
+
const baseUrl = typeof init.baseUrl === "string" && init.baseUrl.trim() ? init.baseUrl.trim() : undefined;
|
|
19
|
+
if (baseUrl && typeof configuration.baseURL !== "string") {
|
|
20
|
+
normalized.configuration = {
|
|
21
|
+
...configuration,
|
|
22
|
+
baseURL: baseUrl,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
delete normalized.baseUrl;
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
export class AgentRuntimeAdapter {
|
|
29
|
+
options;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
async materializeModelStream(streamFactory, input, config) {
|
|
34
|
+
const stream = await streamFactory(input, config);
|
|
35
|
+
let content = "";
|
|
36
|
+
for await (const chunk of stream) {
|
|
37
|
+
const delta = readStreamDelta(chunk) || extractVisibleOutput(chunk);
|
|
38
|
+
if (delta) {
|
|
39
|
+
content += delta;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { content };
|
|
43
|
+
}
|
|
44
|
+
createModelFallbackRunnable(model) {
|
|
45
|
+
return {
|
|
46
|
+
invoke: async (input, config) => {
|
|
47
|
+
const request = typeof input === "object" && input !== null && "messages" in input
|
|
48
|
+
? input.messages
|
|
49
|
+
: input;
|
|
50
|
+
if (typeof model.invoke === "function") {
|
|
51
|
+
return model.invoke(request, config);
|
|
52
|
+
}
|
|
53
|
+
if (typeof model.stream === "function") {
|
|
54
|
+
return this.materializeModelStream(model.stream.bind(model), request, config);
|
|
55
|
+
}
|
|
56
|
+
throw new Error("Resolved model must define invoke or stream.");
|
|
57
|
+
},
|
|
58
|
+
stream: async (input, config) => {
|
|
59
|
+
if (typeof model.stream === "function") {
|
|
60
|
+
const request = typeof input === "object" && input !== null && "messages" in input
|
|
61
|
+
? input.messages
|
|
62
|
+
: input;
|
|
63
|
+
return model.stream(request, config);
|
|
64
|
+
}
|
|
65
|
+
if (typeof model.invoke === "function") {
|
|
66
|
+
const request = typeof input === "object" && input !== null && "messages" in input
|
|
67
|
+
? input.messages
|
|
68
|
+
: input;
|
|
69
|
+
const result = await model.invoke(request, config);
|
|
70
|
+
const text = extractVisibleOutput(result);
|
|
71
|
+
async function* singleChunk() {
|
|
72
|
+
yield { content: text };
|
|
73
|
+
}
|
|
74
|
+
return singleChunk();
|
|
75
|
+
}
|
|
76
|
+
throw new Error("Resolved model must define invoke or stream.");
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
applyStrictToolJsonInstruction(binding) {
|
|
81
|
+
if (binding.langchainAgentParams) {
|
|
82
|
+
return {
|
|
83
|
+
...binding,
|
|
84
|
+
langchainAgentParams: {
|
|
85
|
+
...binding.langchainAgentParams,
|
|
86
|
+
systemPrompt: [binding.langchainAgentParams.systemPrompt, STRICT_TOOL_JSON_INSTRUCTION].filter(Boolean).join("\n\n"),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (binding.deepAgentParams) {
|
|
91
|
+
return {
|
|
92
|
+
...binding,
|
|
93
|
+
deepAgentParams: {
|
|
94
|
+
...binding.deepAgentParams,
|
|
95
|
+
systemPrompt: [binding.deepAgentParams.systemPrompt, STRICT_TOOL_JSON_INSTRUCTION].filter(Boolean).join("\n\n"),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return binding;
|
|
100
|
+
}
|
|
101
|
+
async synthesizeDeepAgentAnswer(binding, input, result) {
|
|
102
|
+
if (!binding.deepAgentParams) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
const toolContext = extractToolFallbackContext(result);
|
|
106
|
+
if (!toolContext) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
const model = (await this.resolveModel(binding.deepAgentParams.model));
|
|
110
|
+
if (!model?.invoke) {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
const synthesized = await model.invoke([
|
|
114
|
+
{
|
|
115
|
+
role: "system",
|
|
116
|
+
content: "The previous agent run completed tool work but did not produce a final user-facing answer. Write the final answer now using the tool results provided. Do not expose internal state, tools, or reasoning.",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
role: "user",
|
|
120
|
+
content: `Original user request:\n${input}\n\nTool results:\n${toolContext}`,
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
return sanitizeVisibleText(extractVisibleOutput(synthesized));
|
|
124
|
+
}
|
|
125
|
+
async resolveModel(model) {
|
|
126
|
+
if (this.options.modelResolver) {
|
|
127
|
+
return wrapResolvedModel(await this.options.modelResolver(model.id));
|
|
128
|
+
}
|
|
129
|
+
if (model.provider === "ollama" && typeof model.init.baseUrl === "string" && model.init.baseUrl.trim()) {
|
|
130
|
+
return wrapResolvedModel(new ChatOllama({ model: model.model, baseUrl: model.init.baseUrl, ...model.init }));
|
|
131
|
+
}
|
|
132
|
+
if (model.provider === "openai-compatible") {
|
|
133
|
+
return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
|
|
134
|
+
}
|
|
135
|
+
if (model.provider === "openai") {
|
|
136
|
+
return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...model.init }));
|
|
137
|
+
}
|
|
138
|
+
if (model.provider === "anthropic") {
|
|
139
|
+
return wrapResolvedModel(new ChatAnthropic({ model: model.model, ...model.init }));
|
|
140
|
+
}
|
|
141
|
+
if (model.provider === "google" || model.provider === "google-genai" || model.provider === "gemini") {
|
|
142
|
+
return wrapResolvedModel(new ChatGoogle({ model: model.model, ...model.init }));
|
|
143
|
+
}
|
|
144
|
+
return wrapResolvedModel(await initChatModel(model.model, { modelProvider: model.provider, ...model.init }));
|
|
145
|
+
}
|
|
146
|
+
resolveTools(tools, binding) {
|
|
147
|
+
const resolved = this.options.toolResolver ? this.options.toolResolver(tools.map((tool) => tool.id), binding) : [];
|
|
148
|
+
return resolved.map((resolvedTool, index) => {
|
|
149
|
+
const compiledTool = tools[index];
|
|
150
|
+
return compiledTool ? wrapToolForExecution(resolvedTool, compiledTool) : resolvedTool;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
normalizeInterruptPolicy(rule) {
|
|
154
|
+
if (!rule)
|
|
155
|
+
return null;
|
|
156
|
+
if (rule === true)
|
|
157
|
+
return ["approve", "edit", "reject"];
|
|
158
|
+
const typed = rule;
|
|
159
|
+
if (Array.isArray(typed.allowedDecisions)) {
|
|
160
|
+
return typed.allowedDecisions.filter((item) => item === "approve" || item === "edit" || item === "reject");
|
|
161
|
+
}
|
|
162
|
+
const decisions = [];
|
|
163
|
+
if (typed.allowAccept !== false)
|
|
164
|
+
decisions.push("approve");
|
|
165
|
+
if (typed.allowEdit !== false)
|
|
166
|
+
decisions.push("edit");
|
|
167
|
+
if (typed.allowReject !== false)
|
|
168
|
+
decisions.push("reject");
|
|
169
|
+
return decisions.length > 0 ? decisions : null;
|
|
170
|
+
}
|
|
171
|
+
compileInterruptOn(_tools, compatibilityRules) {
|
|
172
|
+
const compiled = new Map();
|
|
173
|
+
for (const [toolName, rule] of Object.entries(compatibilityRules ?? {})) {
|
|
174
|
+
const allowedDecisions = this.normalizeInterruptPolicy(rule);
|
|
175
|
+
if (!allowedDecisions) {
|
|
176
|
+
compiled.delete(toolName);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
compiled.set(toolName, { allowedDecisions });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return compiled.size > 0 ? Object.fromEntries(compiled.entries()) : undefined;
|
|
183
|
+
}
|
|
184
|
+
resolveInterruptOn(binding) {
|
|
185
|
+
if (binding.deepAgentParams) {
|
|
186
|
+
return this.compileInterruptOn(binding.deepAgentParams.tools, binding.deepAgentParams.interruptOn);
|
|
187
|
+
}
|
|
188
|
+
return this.compileInterruptOn(binding.langchainAgentParams?.tools ?? [], binding.agent.langchainAgentConfig?.interruptOn);
|
|
189
|
+
}
|
|
190
|
+
resolveMiddleware(binding, interruptOn) {
|
|
191
|
+
const middleware = this.options.middlewareResolver ? this.options.middlewareResolver(binding) : [];
|
|
192
|
+
if (interruptOn && Object.keys(interruptOn).length > 0) {
|
|
193
|
+
middleware.push(humanInTheLoopMiddleware({ interruptOn }));
|
|
194
|
+
}
|
|
195
|
+
return middleware;
|
|
196
|
+
}
|
|
197
|
+
resolveCheckpointer(binding) {
|
|
198
|
+
return this.options.checkpointerResolver ? this.options.checkpointerResolver(binding) : new MemorySaver();
|
|
199
|
+
}
|
|
200
|
+
buildRouteSystemPrompt(primaryBinding, secondaryBinding, overridePrompt) {
|
|
201
|
+
const defaultPrompt = `You are a routing classifier for an agent harness. Reply with exactly one agent id: ${primaryBinding.agent.id} or ${secondaryBinding.agent.id}.\n\n` +
|
|
202
|
+
`Choose ${primaryBinding.agent.id} only for lightweight conversational turns that can be answered directly in one step ` +
|
|
203
|
+
"without tool use, repository inspection, file lookup, external checkout, or orchestration.\n\n" +
|
|
204
|
+
`Choose ${secondaryBinding.agent.id} for tasks that need tools, multi-step execution, external research, repository or ` +
|
|
205
|
+
"file analysis, downloading or cloning content, codebase exploration, verification, or any task where the agent " +
|
|
206
|
+
"should inspect the workspace or another repository before answering.\n\n" +
|
|
207
|
+
`If the request asks to download, clone, fetch, inspect, analyze, trace, or locate implementation in a repo or codebase, choose ${secondaryBinding.agent.id}.\n\n` +
|
|
208
|
+
`When uncertain, prefer ${secondaryBinding.agent.id}.\n\n` +
|
|
209
|
+
`Agent ${primaryBinding.agent.id}: ${primaryBinding.agent.description}\n` +
|
|
210
|
+
`Agent ${secondaryBinding.agent.id}: ${secondaryBinding.agent.description}`;
|
|
211
|
+
if (!overridePrompt?.trim()) {
|
|
212
|
+
return defaultPrompt;
|
|
213
|
+
}
|
|
214
|
+
return overridePrompt
|
|
215
|
+
.replaceAll("{{primaryAgentId}}", primaryBinding.agent.id)
|
|
216
|
+
.replaceAll("{{primaryDescription}}", primaryBinding.agent.description)
|
|
217
|
+
.replaceAll("{{secondaryAgentId}}", secondaryBinding.agent.id)
|
|
218
|
+
.replaceAll("{{secondaryDescription}}", secondaryBinding.agent.description);
|
|
219
|
+
}
|
|
220
|
+
async resolveSubagents(subagents) {
|
|
221
|
+
return Promise.all(subagents.map(async (subagent) => ({
|
|
222
|
+
...subagent,
|
|
223
|
+
model: subagent.model ? (await this.resolveModel(subagent.model)) : undefined,
|
|
224
|
+
tools: subagent.tools ? this.resolveTools(subagent.tools) : undefined,
|
|
225
|
+
interruptOn: this.compileInterruptOn(subagent.tools ?? [], subagent.interruptOn),
|
|
226
|
+
})));
|
|
227
|
+
}
|
|
228
|
+
buildConversation(systemPrompt, history, input) {
|
|
229
|
+
const messages = [];
|
|
230
|
+
if (systemPrompt) {
|
|
231
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
232
|
+
}
|
|
233
|
+
for (const item of history) {
|
|
234
|
+
messages.push({ role: item.role, content: item.content });
|
|
235
|
+
}
|
|
236
|
+
messages.push({ role: "user", content: input });
|
|
237
|
+
return messages;
|
|
238
|
+
}
|
|
239
|
+
async create(binding) {
|
|
240
|
+
if (binding.langchainAgentParams) {
|
|
241
|
+
const interruptOn = this.resolveInterruptOn(binding);
|
|
242
|
+
const model = (await this.resolveModel(binding.langchainAgentParams.model));
|
|
243
|
+
const tools = this.resolveTools(binding.langchainAgentParams.tools, binding);
|
|
244
|
+
if (tools.length > 0 && typeof model.bindTools !== "function") {
|
|
245
|
+
return this.createModelFallbackRunnable(model);
|
|
246
|
+
}
|
|
247
|
+
return createAgent({
|
|
248
|
+
model: model,
|
|
249
|
+
tools: tools,
|
|
250
|
+
systemPrompt: binding.langchainAgentParams.systemPrompt,
|
|
251
|
+
middleware: this.resolveMiddleware(binding, interruptOn),
|
|
252
|
+
checkpointer: this.resolveCheckpointer(binding),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const params = binding.deepAgentParams;
|
|
256
|
+
if (!params) {
|
|
257
|
+
throw new Error(`Agent ${binding.agent.id} has no runnable params`);
|
|
258
|
+
}
|
|
259
|
+
return createDeepAgent({
|
|
260
|
+
model: (await this.resolveModel(params.model)),
|
|
261
|
+
tools: this.resolveTools(params.tools, binding),
|
|
262
|
+
systemPrompt: params.systemPrompt,
|
|
263
|
+
middleware: this.resolveMiddleware(binding),
|
|
264
|
+
subagents: (await this.resolveSubagents(params.subagents)),
|
|
265
|
+
checkpointer: this.resolveCheckpointer(binding),
|
|
266
|
+
store: this.options.storeResolver?.(binding),
|
|
267
|
+
backend: this.options.backendResolver?.(binding),
|
|
268
|
+
interruptOn: this.resolveInterruptOn(binding),
|
|
269
|
+
name: params.name,
|
|
270
|
+
memory: params.memory,
|
|
271
|
+
skills: params.skills,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async route(input, primaryBinding, secondaryBinding, options = {}) {
|
|
275
|
+
const routeModelConfig = primaryBinding.langchainAgentParams?.model ??
|
|
276
|
+
primaryBinding.deepAgentParams?.model ??
|
|
277
|
+
secondaryBinding.langchainAgentParams?.model ??
|
|
278
|
+
secondaryBinding.deepAgentParams?.model;
|
|
279
|
+
if (!routeModelConfig) {
|
|
280
|
+
throw new Error("No router model configuration available");
|
|
281
|
+
}
|
|
282
|
+
const routerModel = (await this.resolveModel(routeModelConfig));
|
|
283
|
+
if (!routerModel?.invoke) {
|
|
284
|
+
throw new Error("Router model does not support invoke()");
|
|
285
|
+
}
|
|
286
|
+
const result = await routerModel.invoke([
|
|
287
|
+
{
|
|
288
|
+
role: "system",
|
|
289
|
+
content: this.buildRouteSystemPrompt(primaryBinding, secondaryBinding, options.systemPrompt),
|
|
290
|
+
},
|
|
291
|
+
{ role: "user", content: input },
|
|
292
|
+
]);
|
|
293
|
+
const content = typeof result === "string"
|
|
294
|
+
? result
|
|
295
|
+
: typeof result?.content === "string"
|
|
296
|
+
? result.content
|
|
297
|
+
: JSON.stringify(result);
|
|
298
|
+
const normalized = content.trim().toLowerCase();
|
|
299
|
+
return normalized === secondaryBinding.agent.id.toLowerCase() || normalized.includes(secondaryBinding.agent.id.toLowerCase())
|
|
300
|
+
? secondaryBinding.agent.id
|
|
301
|
+
: primaryBinding.agent.id;
|
|
302
|
+
}
|
|
303
|
+
async invoke(binding, input, threadId, runId, resumePayload, history = []) {
|
|
304
|
+
const request = resumePayload === undefined
|
|
305
|
+
? { messages: this.buildConversation(binding.langchainAgentParams?.systemPrompt ?? binding.deepAgentParams?.systemPrompt, history, input) }
|
|
306
|
+
: new Command({ resume: resumePayload });
|
|
307
|
+
let result;
|
|
308
|
+
try {
|
|
309
|
+
const runnable = await this.create(binding);
|
|
310
|
+
result = (await runnable.invoke(request, { configurable: { thread_id: threadId } }));
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (resumePayload !== undefined || !isToolCallParseFailure(error)) {
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
const retriedBinding = this.applyStrictToolJsonInstruction(binding);
|
|
317
|
+
const runnable = await this.create(retriedBinding);
|
|
318
|
+
result = (await runnable.invoke({ messages: this.buildConversation(retriedBinding.langchainAgentParams?.systemPrompt ?? retriedBinding.deepAgentParams?.systemPrompt, history, input) }, { configurable: { thread_id: threadId } }));
|
|
319
|
+
}
|
|
320
|
+
const interruptContent = Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? JSON.stringify(result.__interrupt__) : undefined;
|
|
321
|
+
const extractedOutput = extractVisibleOutput(result);
|
|
322
|
+
const visibleOutput = extractedOutput && !isLikelyToolArgsObject(tryParseJson(extractedOutput)) ? extractedOutput : "";
|
|
323
|
+
const toolFallback = extractToolFallbackContext(result);
|
|
324
|
+
const output = visibleOutput || (await this.synthesizeDeepAgentAnswer(binding, input, result)) || toolFallback || JSON.stringify(result, null, 2);
|
|
325
|
+
return {
|
|
326
|
+
threadId,
|
|
327
|
+
runId,
|
|
328
|
+
agentId: binding.agent.id,
|
|
329
|
+
state: Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? "waiting_for_approval" : "completed",
|
|
330
|
+
interruptContent,
|
|
331
|
+
output: sanitizeVisibleText(output),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async *stream(binding, input, threadId, history = []) {
|
|
335
|
+
try {
|
|
336
|
+
if (binding.langchainAgentParams) {
|
|
337
|
+
const resolvedModel = (await this.resolveModel(binding.langchainAgentParams.model));
|
|
338
|
+
const tools = this.resolveTools(binding.langchainAgentParams.tools, binding);
|
|
339
|
+
const canUseDirectModelStream = tools.length === 0 || typeof resolvedModel.bindTools !== "function";
|
|
340
|
+
const model = canUseDirectModelStream
|
|
341
|
+
? resolvedModel
|
|
342
|
+
: typeof resolvedModel.bindTools === "function" && tools.length > 0
|
|
343
|
+
? resolvedModel.bindTools(tools)
|
|
344
|
+
: resolvedModel;
|
|
345
|
+
// For tool-using langchain agents, a raw model.stream pass cannot execute the
|
|
346
|
+
// agent loop and only adds an extra model round-trip before the runnable path.
|
|
347
|
+
if (canUseDirectModelStream && typeof model.stream === "function") {
|
|
348
|
+
let emitted = false;
|
|
349
|
+
const stream = await model.stream(this.buildConversation(binding.langchainAgentParams.systemPrompt, history, input));
|
|
350
|
+
for await (const chunk of stream) {
|
|
351
|
+
const delta = readStreamDelta(chunk);
|
|
352
|
+
if (delta) {
|
|
353
|
+
emitted = true;
|
|
354
|
+
yield { kind: "content", content: delta };
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const reasoning = extractReasoningText(chunk);
|
|
358
|
+
if (reasoning) {
|
|
359
|
+
yield { kind: "reasoning", content: reasoning };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (emitted) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const runnable = await this.create(binding);
|
|
368
|
+
const request = { messages: this.buildConversation(binding.deepAgentParams?.systemPrompt, history, input) };
|
|
369
|
+
if (typeof runnable.streamEvents === "function") {
|
|
370
|
+
const events = await runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2" });
|
|
371
|
+
let terminalOutput = "";
|
|
372
|
+
const seenTerminalOutputs = new Set();
|
|
373
|
+
let lastStep = "";
|
|
374
|
+
for await (const event of events) {
|
|
375
|
+
const interruptPayload = extractInterruptPayload(event);
|
|
376
|
+
if (interruptPayload) {
|
|
377
|
+
yield { kind: "interrupt", content: interruptPayload };
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const reasoning = extractReasoningStreamOutput(event);
|
|
381
|
+
if (reasoning) {
|
|
382
|
+
yield { kind: "reasoning", content: reasoning };
|
|
383
|
+
}
|
|
384
|
+
const agentStep = extractAgentStep(event);
|
|
385
|
+
if (agentStep && agentStep !== lastStep) {
|
|
386
|
+
lastStep = agentStep;
|
|
387
|
+
yield { kind: "step", content: agentStep };
|
|
388
|
+
}
|
|
389
|
+
const toolResult = extractToolResult(event);
|
|
390
|
+
if (toolResult) {
|
|
391
|
+
yield { kind: "tool-result", toolName: toolResult.toolName, output: toolResult.output };
|
|
392
|
+
}
|
|
393
|
+
const output = extractTerminalStreamOutput(event);
|
|
394
|
+
if (output) {
|
|
395
|
+
const outputKey = normalizeTerminalOutputKey(output);
|
|
396
|
+
if (outputKey && seenTerminalOutputs.has(outputKey)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (outputKey) {
|
|
400
|
+
seenTerminalOutputs.add(outputKey);
|
|
401
|
+
}
|
|
402
|
+
terminalOutput += output;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (terminalOutput) {
|
|
406
|
+
yield { kind: "content", content: sanitizeVisibleText(terminalOutput) };
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (binding.langchainAgentParams && typeof runnable.stream === "function") {
|
|
411
|
+
const stream = await runnable.stream(request, { configurable: { thread_id: threadId } });
|
|
412
|
+
let emitted = false;
|
|
413
|
+
for await (const chunk of stream) {
|
|
414
|
+
const delta = readStreamDelta(chunk);
|
|
415
|
+
if (delta) {
|
|
416
|
+
emitted = true;
|
|
417
|
+
yield { kind: "content", content: delta };
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const reasoning = extractReasoningText(chunk);
|
|
421
|
+
if (reasoning) {
|
|
422
|
+
yield { kind: "reasoning", content: reasoning };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (emitted) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const result = await this.invoke(binding, input, threadId, threadId);
|
|
430
|
+
if (result.output) {
|
|
431
|
+
yield { kind: "content", content: sanitizeVisibleText(result.output) };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (!isToolCallParseFailure(error)) {
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
const retried = await this.invoke(this.applyStrictToolJsonInstruction(binding), input, threadId, threadId, undefined, history);
|
|
439
|
+
if (retried.output) {
|
|
440
|
+
yield { kind: "content", content: sanitizeVisibleText(retried.output) };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
export { AgentRuntimeAdapter as RuntimeAdapter, AGENT_INTERRUPT_SENTINEL_PREFIX, AGENT_INTERRUPT_SENTINEL_PREFIX as INTERRUPT_SENTINEL_PREFIX, };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { getEventSubscribers } from "../extensions.js";
|
|
3
|
+
export class EventBus {
|
|
4
|
+
emitter = new EventEmitter();
|
|
5
|
+
publish(event) {
|
|
6
|
+
this.emitter.emit("event", event);
|
|
7
|
+
for (const subscriber of getEventSubscribers()) {
|
|
8
|
+
void Promise.resolve(subscriber.onEvent(event));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
subscribe(listener) {
|
|
12
|
+
this.emitter.on("event", listener);
|
|
13
|
+
return () => this.emitter.off("event", listener);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
2
|
+
type MemorySaverConfig = Parameters<MemorySaver["getTuple"]>[0];
|
|
3
|
+
type MemorySaverListOptions = Parameters<MemorySaver["list"]>[1];
|
|
4
|
+
type MemorySaverPutCheckpoint = Parameters<MemorySaver["put"]>[1];
|
|
5
|
+
type MemorySaverPutMetadata = Parameters<MemorySaver["put"]>[2];
|
|
6
|
+
type MemorySaverPutWrites = Parameters<MemorySaver["putWrites"]>[1];
|
|
7
|
+
type MemorySaverPutResult = ReturnType<MemorySaver["put"]>;
|
|
8
|
+
export declare class FileCheckpointSaver extends MemorySaver {
|
|
9
|
+
private readonly filePath;
|
|
10
|
+
private loaded;
|
|
11
|
+
constructor(filePath: string);
|
|
12
|
+
private ensureLoaded;
|
|
13
|
+
private persist;
|
|
14
|
+
getTuple(config: MemorySaverConfig): Promise<import("@langchain/langgraph").CheckpointTuple | undefined>;
|
|
15
|
+
list(config: MemorySaverConfig, options?: MemorySaverListOptions): AsyncGenerator<import("@langchain/langgraph").CheckpointTuple, void, unknown>;
|
|
16
|
+
put(config: MemorySaverConfig, checkpoint: MemorySaverPutCheckpoint, metadata: MemorySaverPutMetadata): MemorySaverPutResult;
|
|
17
|
+
putWrites(config: MemorySaverConfig, writes: MemorySaverPutWrites, taskId: string): Promise<void>;
|
|
18
|
+
deleteThread(threadId: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export { FileCheckpointSaver as FileCheckpointer };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
4
|
+
function encodeBinary(value) {
|
|
5
|
+
if (value instanceof Uint8Array) {
|
|
6
|
+
return {
|
|
7
|
+
__type: "Uint8Array",
|
|
8
|
+
data: Array.from(value),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value.map((item) => encodeBinary(item));
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === "object" && value) {
|
|
15
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, encodeBinary(entry)]));
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function decodeBinary(value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.map((item) => decodeBinary(item));
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "object" && value) {
|
|
24
|
+
const typed = value;
|
|
25
|
+
if (typed.__type === "Uint8Array" && Array.isArray(typed.data)) {
|
|
26
|
+
return new Uint8Array(typed.data.map((item) => Number(item)));
|
|
27
|
+
}
|
|
28
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeBinary(entry)]));
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
export class FileCheckpointSaver extends MemorySaver {
|
|
33
|
+
filePath;
|
|
34
|
+
loaded = false;
|
|
35
|
+
constructor(filePath) {
|
|
36
|
+
super();
|
|
37
|
+
this.filePath = filePath;
|
|
38
|
+
}
|
|
39
|
+
async ensureLoaded() {
|
|
40
|
+
if (this.loaded) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(this.filePath, "utf8");
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
this.storage = decodeBinary(parsed.storage ?? {});
|
|
47
|
+
this.writes = decodeBinary(parsed.writes ?? {});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
this.storage = {};
|
|
51
|
+
this.writes = {};
|
|
52
|
+
}
|
|
53
|
+
this.loaded = true;
|
|
54
|
+
}
|
|
55
|
+
async persist() {
|
|
56
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
57
|
+
await writeFile(this.filePath, JSON.stringify({
|
|
58
|
+
storage: this.storage,
|
|
59
|
+
writes: this.writes,
|
|
60
|
+
}, (_, value) => encodeBinary(value), 2), "utf8");
|
|
61
|
+
}
|
|
62
|
+
async getTuple(config) {
|
|
63
|
+
await this.ensureLoaded();
|
|
64
|
+
return super.getTuple(config);
|
|
65
|
+
}
|
|
66
|
+
async *list(config, options) {
|
|
67
|
+
await this.ensureLoaded();
|
|
68
|
+
for await (const item of super.list(config, options)) {
|
|
69
|
+
yield item;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async put(config, checkpoint, metadata) {
|
|
73
|
+
await this.ensureLoaded();
|
|
74
|
+
const result = await super.put(config, checkpoint, metadata);
|
|
75
|
+
await this.persist();
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
async putWrites(config, writes, taskId) {
|
|
79
|
+
await this.ensureLoaded();
|
|
80
|
+
const result = await super.putWrites(config, writes, taskId);
|
|
81
|
+
await this.persist();
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
async deleteThread(threadId) {
|
|
85
|
+
await this.ensureLoaded();
|
|
86
|
+
const result = await super.deleteThread(threadId);
|
|
87
|
+
await this.persist();
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export { FileCheckpointSaver as FileCheckpointer };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import type { ApprovalRecord, ArtifactListing, DelegationRecord, HarnessEvent, HarnessStreamItem, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, SessionRecord, WorkspaceBundle } from "../contracts/types.js";
|
|
3
|
+
export declare class AgentHarness {
|
|
4
|
+
private readonly workspace;
|
|
5
|
+
private readonly runtimeAdapterOptions;
|
|
6
|
+
private readonly eventBus;
|
|
7
|
+
private readonly persistence;
|
|
8
|
+
private readonly policyEngine;
|
|
9
|
+
private readonly runtimeAdapter;
|
|
10
|
+
private readonly checkpointers;
|
|
11
|
+
private readonly stores;
|
|
12
|
+
private readonly embeddingModels;
|
|
13
|
+
private readonly vectorStores;
|
|
14
|
+
private readonly defaultStore;
|
|
15
|
+
private readonly routingSystemPrompt?;
|
|
16
|
+
private readonly threadMemorySync;
|
|
17
|
+
private readonly unsubscribeThreadMemorySync;
|
|
18
|
+
private listHostBindings;
|
|
19
|
+
private defaultRunRoot;
|
|
20
|
+
private heuristicRoute;
|
|
21
|
+
private buildRoutingInput;
|
|
22
|
+
private resolveSelectedAgentId;
|
|
23
|
+
private resolveStore;
|
|
24
|
+
private resolveEmbeddingModel;
|
|
25
|
+
private resolveVectorStore;
|
|
26
|
+
constructor(workspace: WorkspaceBundle, runtimeAdapterOptions?: RuntimeAdapterOptions);
|
|
27
|
+
initialize(): Promise<void>;
|
|
28
|
+
subscribeEvents(listener: (event: HarnessEvent) => void): () => void;
|
|
29
|
+
subscribe(listener: (event: HarnessEvent) => void): () => void;
|
|
30
|
+
listSessions(filter?: {
|
|
31
|
+
agentId?: string;
|
|
32
|
+
}): Promise<SessionRecord[]>;
|
|
33
|
+
getSession(threadId: string): Promise<SessionRecord | null>;
|
|
34
|
+
getEvents(threadId: string, runId?: string): Promise<HarnessEvent[]>;
|
|
35
|
+
listPendingApprovals(): Promise<ApprovalRecord[]>;
|
|
36
|
+
getApproval(approvalId: string): Promise<ApprovalRecord | null>;
|
|
37
|
+
listDelegations(): Promise<DelegationRecord[]>;
|
|
38
|
+
routeAgent(input: string, options?: {
|
|
39
|
+
threadId?: string;
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
artifacts(threadId: string, runId?: string): Promise<ArtifactListing>;
|
|
42
|
+
private emit;
|
|
43
|
+
private persistApproval;
|
|
44
|
+
private resolveApprovalRecord;
|
|
45
|
+
run(options: RunOptions): Promise<RunResult>;
|
|
46
|
+
stream(options: RunOptions): AsyncGenerator<string>;
|
|
47
|
+
streamEvents(options: RunOptions): AsyncGenerator<HarnessStreamItem>;
|
|
48
|
+
streamReadable(options: RunOptions): Promise<Readable>;
|
|
49
|
+
resume(options: ResumeOptions): Promise<RunResult>;
|
|
50
|
+
submitDecision(options: ResumeOptions): Promise<RunResult>;
|
|
51
|
+
approve(threadId: string, runId?: string): Promise<RunResult>;
|
|
52
|
+
reject(threadId: string, runId?: string): Promise<RunResult>;
|
|
53
|
+
restartConversation(options: RestartConversationOptions): Promise<RunResult & {
|
|
54
|
+
restart: Record<string, string>;
|
|
55
|
+
}>;
|
|
56
|
+
close(): Promise<void>;
|
|
57
|
+
}
|