@botbotgo/agent-harness 0.0.39 → 0.0.41
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 +12 -0
- package/dist/config/workspace.yaml +15 -1
- package/dist/contracts/types.d.ts +9 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/persistence/file-store.d.ts +36 -0
- package/dist/persistence/file-store.js +32 -0
- package/dist/runtime/agent-runtime-adapter.d.ts +11 -2
- package/dist/runtime/agent-runtime-adapter.js +22 -7
- package/dist/runtime/declared-middleware.js +39 -4
- package/dist/runtime/harness.d.ts +5 -0
- package/dist/runtime/harness.js +131 -33
- package/dist/workspace/agent-binding-compiler.js +26 -0
- package/dist/workspace/compile.js +16 -1
- package/dist/workspace/object-loader.js +5 -0
- package/dist/workspace/support/workspace-ref-utils.d.ts +11 -0
- package/dist/workspace/support/workspace-ref-utils.js +26 -0
- package/dist/workspace/validate.js +6 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -111,11 +111,23 @@ import { run } from "@botbotgo/agent-harness";
|
|
|
111
111
|
const result = await run(runtime, {
|
|
112
112
|
agentId: "orchestra",
|
|
113
113
|
input: "Explain the available agents in this workspace.",
|
|
114
|
+
context: {
|
|
115
|
+
requestId: "req-123",
|
|
116
|
+
},
|
|
117
|
+
state: {
|
|
118
|
+
visitCount: 1,
|
|
119
|
+
},
|
|
114
120
|
});
|
|
115
121
|
```
|
|
116
122
|
|
|
117
123
|
Each run creates or continues a persisted thread. The result includes `threadId`, `runId`, `state`, and `output`.
|
|
118
124
|
|
|
125
|
+
When the underlying LangChain or DeepAgents graph expects extra invocation data, `run(...)` can pass it through without inventing a parallel harness abstraction:
|
|
126
|
+
|
|
127
|
+
- `context`: forwarded as LangChain v1 runtime context
|
|
128
|
+
- `state`: merged into the agent invocation input for stateful graphs
|
|
129
|
+
- `files`: merged into the agent invocation input so DeepAgents state-backed skills or memories can be provided at call time
|
|
130
|
+
|
|
119
131
|
### Let The Runtime Route
|
|
120
132
|
|
|
121
133
|
```ts
|
|
@@ -58,7 +58,7 @@ spec:
|
|
|
58
58
|
# regex:
|
|
59
59
|
# - "\\b(create|build|implement|fix|review|debug|inspect|analy[sz]e|download|clone)\\b"
|
|
60
60
|
rules:
|
|
61
|
-
- agentId:
|
|
61
|
+
- agentId: orchestra
|
|
62
62
|
contains: ["latest", "recent", "today", "current", "news", "最新", "最近", "今天", "当前", "新闻", "头条", "研究", "调研"]
|
|
63
63
|
- agentId: orchestra
|
|
64
64
|
regex:
|
|
@@ -102,3 +102,17 @@ spec:
|
|
|
102
102
|
# sqlite:
|
|
103
103
|
# sweepBatchSize: 200
|
|
104
104
|
# vacuum: false
|
|
105
|
+
|
|
106
|
+
# agent-harness feature: runtime-managed recovery policy for interrupted runs.
|
|
107
|
+
# This keeps checkpoint resume as an internal lifecycle concern instead of a primary user-facing API concept.
|
|
108
|
+
#
|
|
109
|
+
# Current support:
|
|
110
|
+
# - startup recovery of runs already in `resuming` state
|
|
111
|
+
# - persisted approval-decision intent for cross-restart resume continuation
|
|
112
|
+
# - bounded retry attempts to avoid infinite restart loops
|
|
113
|
+
#
|
|
114
|
+
# Example:
|
|
115
|
+
# recovery:
|
|
116
|
+
# enabled: true
|
|
117
|
+
# resumeResumingRunsOnStartup: true
|
|
118
|
+
# maxRecoveryAttempts: 3
|
|
@@ -86,9 +86,13 @@ export type LangChainAgentParams = {
|
|
|
86
86
|
model: CompiledModel;
|
|
87
87
|
tools: CompiledTool[];
|
|
88
88
|
systemPrompt?: string;
|
|
89
|
+
stateSchema?: unknown;
|
|
89
90
|
responseFormat?: unknown;
|
|
90
91
|
contextSchema?: unknown;
|
|
91
92
|
middleware?: Array<Record<string, unknown>>;
|
|
93
|
+
includeAgentName?: "inline";
|
|
94
|
+
version?: "v1" | "v2";
|
|
95
|
+
name?: string;
|
|
92
96
|
description: string;
|
|
93
97
|
};
|
|
94
98
|
export type CompiledSubAgent = {
|
|
@@ -119,6 +123,8 @@ export type DeepAgentParams = {
|
|
|
119
123
|
name: string;
|
|
120
124
|
memory: string[];
|
|
121
125
|
skills: string[];
|
|
126
|
+
generalPurposeAgent?: boolean;
|
|
127
|
+
taskDescription?: string;
|
|
122
128
|
};
|
|
123
129
|
export type CompiledModel = {
|
|
124
130
|
id: string;
|
|
@@ -262,6 +268,9 @@ export type RunStartOptions = {
|
|
|
262
268
|
agentId?: string;
|
|
263
269
|
input: MessageContent;
|
|
264
270
|
threadId?: string;
|
|
271
|
+
context?: Record<string, unknown>;
|
|
272
|
+
state?: Record<string, unknown>;
|
|
273
|
+
files?: Record<string, unknown>;
|
|
265
274
|
listeners?: RunListeners;
|
|
266
275
|
};
|
|
267
276
|
export type RunDecisionOptions = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.40";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.40";
|
|
@@ -8,6 +8,36 @@ type ThreadMeta = {
|
|
|
8
8
|
status: RunState;
|
|
9
9
|
latestRunId: string;
|
|
10
10
|
};
|
|
11
|
+
type RunMeta = {
|
|
12
|
+
runId: string;
|
|
13
|
+
threadId: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
executionMode: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
};
|
|
19
|
+
type Lifecycle = {
|
|
20
|
+
state: RunState;
|
|
21
|
+
previousState: RunState | null;
|
|
22
|
+
stateEnteredAt: string;
|
|
23
|
+
lastTransitionAt: string;
|
|
24
|
+
resumable: boolean;
|
|
25
|
+
checkpointRef: string | null;
|
|
26
|
+
};
|
|
27
|
+
type RunIndexRecord = {
|
|
28
|
+
runId: string;
|
|
29
|
+
threadId: string;
|
|
30
|
+
state: RunState;
|
|
31
|
+
resumable: boolean;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
};
|
|
34
|
+
type RecoveryIntent = {
|
|
35
|
+
kind: "approval-decision";
|
|
36
|
+
savedAt: string;
|
|
37
|
+
checkpointRef: string | null;
|
|
38
|
+
resumePayload: unknown;
|
|
39
|
+
attempts: number;
|
|
40
|
+
};
|
|
11
41
|
export declare class FilePersistence {
|
|
12
42
|
private readonly runRoot;
|
|
13
43
|
constructor(runRoot: string);
|
|
@@ -31,6 +61,7 @@ export declare class FilePersistence {
|
|
|
31
61
|
setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
|
|
32
62
|
appendEvent(event: HarnessEvent): Promise<void>;
|
|
33
63
|
listSessions(): Promise<ThreadSummary[]>;
|
|
64
|
+
listRunIndexes(): Promise<RunIndexRecord[]>;
|
|
34
65
|
getSession(threadId: string): Promise<ThreadSummary | null>;
|
|
35
66
|
getThreadMeta(threadId: string): Promise<ThreadMeta | null>;
|
|
36
67
|
listThreadRuns(threadId: string): Promise<ThreadRunRecord[]>;
|
|
@@ -38,6 +69,8 @@ export declare class FilePersistence {
|
|
|
38
69
|
listApprovals(): Promise<ApprovalRecord[]>;
|
|
39
70
|
getApproval(approvalId: string): Promise<ApprovalRecord | null>;
|
|
40
71
|
getRunApprovals(threadId: string, runId: string): Promise<ApprovalRecord[]>;
|
|
72
|
+
getRunMeta(threadId: string, runId: string): Promise<RunMeta>;
|
|
73
|
+
getRunLifecycle(threadId: string, runId: string): Promise<Lifecycle>;
|
|
41
74
|
listDelegations(): Promise<DelegationRecord[]>;
|
|
42
75
|
createApproval(record: ApprovalRecord): Promise<void>;
|
|
43
76
|
resolveApproval(threadId: string, runId: string, approvalId: string, status: ApprovalRecord["status"]): Promise<ApprovalRecord>;
|
|
@@ -47,5 +80,8 @@ export declare class FilePersistence {
|
|
|
47
80
|
listArtifacts(threadId: string, runId: string): Promise<ArtifactListing>;
|
|
48
81
|
appendThreadMessage(threadId: string, message: TranscriptMessage): Promise<void>;
|
|
49
82
|
listThreadMessages(threadId: string, limit?: number): Promise<TranscriptMessage[]>;
|
|
83
|
+
saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
|
|
84
|
+
getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
|
|
85
|
+
clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
|
|
50
86
|
}
|
|
51
87
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
2
3
|
import { readdir } from "node:fs/promises";
|
|
3
4
|
import { ensureDir, fileExists, readJson, writeJson } from "../utils/fs.js";
|
|
4
5
|
export class FilePersistence {
|
|
@@ -168,6 +169,14 @@ export class FilePersistence {
|
|
|
168
169
|
}));
|
|
169
170
|
return records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
170
171
|
}
|
|
172
|
+
async listRunIndexes() {
|
|
173
|
+
const runIndexDir = path.join(this.runRoot, "indexes", "runs");
|
|
174
|
+
if (!(await fileExists(runIndexDir))) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const entries = (await readdir(runIndexDir)).sort();
|
|
178
|
+
return Promise.all(entries.map((entry) => readJson(path.join(runIndexDir, entry))));
|
|
179
|
+
}
|
|
171
180
|
async getSession(threadId) {
|
|
172
181
|
const filePath = path.join(this.runRoot, "indexes", "threads", `${threadId}.json`);
|
|
173
182
|
if (!(await fileExists(filePath))) {
|
|
@@ -249,6 +258,12 @@ export class FilePersistence {
|
|
|
249
258
|
const entries = (await readdir(approvalsDir)).sort();
|
|
250
259
|
return Promise.all(entries.map((entry) => readJson(path.join(approvalsDir, entry))));
|
|
251
260
|
}
|
|
261
|
+
async getRunMeta(threadId, runId) {
|
|
262
|
+
return readJson(path.join(this.runDir(threadId, runId), "meta.json"));
|
|
263
|
+
}
|
|
264
|
+
async getRunLifecycle(threadId, runId) {
|
|
265
|
+
return readJson(path.join(this.runDir(threadId, runId), "lifecycle.json"));
|
|
266
|
+
}
|
|
252
267
|
async listDelegations() {
|
|
253
268
|
const delegationsDir = path.join(this.runRoot, "indexes", "delegations");
|
|
254
269
|
if (!(await fileExists(delegationsDir))) {
|
|
@@ -321,4 +336,21 @@ export class FilePersistence {
|
|
|
321
336
|
const current = await readJson(messagesPath);
|
|
322
337
|
return current.items.slice(-limit);
|
|
323
338
|
}
|
|
339
|
+
async saveRecoveryIntent(threadId, runId, intent) {
|
|
340
|
+
await writeJson(path.join(this.runDir(threadId, runId), "recovery-intent.json"), intent);
|
|
341
|
+
}
|
|
342
|
+
async getRecoveryIntent(threadId, runId) {
|
|
343
|
+
const intentPath = path.join(this.runDir(threadId, runId), "recovery-intent.json");
|
|
344
|
+
if (!(await fileExists(intentPath))) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return readJson(intentPath);
|
|
348
|
+
}
|
|
349
|
+
async clearRecoveryIntent(threadId, runId) {
|
|
350
|
+
const intentPath = path.join(this.runDir(threadId, runId), "recovery-intent.json");
|
|
351
|
+
if (!(await fileExists(intentPath))) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
await rm(intentPath, { force: true });
|
|
355
|
+
}
|
|
324
356
|
}
|
|
@@ -27,6 +27,7 @@ export declare class AgentRuntimeAdapter {
|
|
|
27
27
|
private resolveModel;
|
|
28
28
|
private buildToolNameMapping;
|
|
29
29
|
private buildAgentMessages;
|
|
30
|
+
private buildInvocationRequest;
|
|
30
31
|
private buildRawModelMessages;
|
|
31
32
|
private resolveTools;
|
|
32
33
|
private normalizeInterruptPolicy;
|
|
@@ -40,7 +41,15 @@ export declare class AgentRuntimeAdapter {
|
|
|
40
41
|
route(input: MessageContent, primaryBinding: CompiledAgentBinding, secondaryBinding: CompiledAgentBinding, options?: {
|
|
41
42
|
systemPrompt?: string;
|
|
42
43
|
}): Promise<string>;
|
|
43
|
-
invoke(binding: CompiledAgentBinding, input: MessageContent, threadId: string, runId: string, resumePayload?: unknown, history?: TranscriptMessage[]
|
|
44
|
-
|
|
44
|
+
invoke(binding: CompiledAgentBinding, input: MessageContent, threadId: string, runId: string, resumePayload?: unknown, history?: TranscriptMessage[], options?: {
|
|
45
|
+
context?: Record<string, unknown>;
|
|
46
|
+
state?: Record<string, unknown>;
|
|
47
|
+
files?: Record<string, unknown>;
|
|
48
|
+
}): Promise<RunResult>;
|
|
49
|
+
stream(binding: CompiledAgentBinding, input: MessageContent, threadId: string, history?: TranscriptMessage[], options?: {
|
|
50
|
+
context?: Record<string, unknown>;
|
|
51
|
+
state?: Record<string, unknown>;
|
|
52
|
+
files?: Record<string, unknown>;
|
|
53
|
+
}): AsyncGenerator<RuntimeStreamChunk | string>;
|
|
45
54
|
}
|
|
46
55
|
export { AgentRuntimeAdapter as RuntimeAdapter, AGENT_INTERRUPT_SENTINEL_PREFIX, AGENT_INTERRUPT_SENTINEL_PREFIX as INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError, };
|
|
@@ -377,6 +377,13 @@ export class AgentRuntimeAdapter {
|
|
|
377
377
|
{ role: "user", content: normalizeMessageContent(input) },
|
|
378
378
|
];
|
|
379
379
|
}
|
|
380
|
+
buildInvocationRequest(history, input, options = {}) {
|
|
381
|
+
return {
|
|
382
|
+
...(options.state ?? {}),
|
|
383
|
+
...(options.files ? { files: options.files } : {}),
|
|
384
|
+
messages: this.buildAgentMessages(history, input),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
380
387
|
buildRawModelMessages(systemPrompt, history, input) {
|
|
381
388
|
const messages = [];
|
|
382
389
|
if (systemPrompt) {
|
|
@@ -503,10 +510,16 @@ export class AgentRuntimeAdapter {
|
|
|
503
510
|
model: model,
|
|
504
511
|
tools: tools,
|
|
505
512
|
systemPrompt: binding.langchainAgentParams.systemPrompt,
|
|
513
|
+
stateSchema: binding.langchainAgentParams.stateSchema,
|
|
506
514
|
responseFormat: binding.langchainAgentParams.responseFormat,
|
|
507
515
|
contextSchema: binding.langchainAgentParams.contextSchema,
|
|
508
516
|
middleware: (await this.resolveMiddleware(binding, interruptOn)),
|
|
509
517
|
checkpointer: this.resolveCheckpointer(binding),
|
|
518
|
+
store: this.options.storeResolver?.(binding),
|
|
519
|
+
includeAgentName: binding.langchainAgentParams.includeAgentName,
|
|
520
|
+
version: binding.langchainAgentParams.version,
|
|
521
|
+
name: binding.langchainAgentParams.name,
|
|
522
|
+
description: binding.langchainAgentParams.description,
|
|
510
523
|
});
|
|
511
524
|
}
|
|
512
525
|
const params = binding.deepAgentParams;
|
|
@@ -545,6 +558,8 @@ export class AgentRuntimeAdapter {
|
|
|
545
558
|
name: params.name,
|
|
546
559
|
memory: params.memory,
|
|
547
560
|
skills: params.skills,
|
|
561
|
+
generalPurposeAgent: params.generalPurposeAgent,
|
|
562
|
+
taskDescription: params.taskDescription,
|
|
548
563
|
};
|
|
549
564
|
return createDeepAgent(deepAgentConfig);
|
|
550
565
|
}
|
|
@@ -577,14 +592,14 @@ export class AgentRuntimeAdapter {
|
|
|
577
592
|
? secondaryBinding.agent.id
|
|
578
593
|
: primaryBinding.agent.id;
|
|
579
594
|
}
|
|
580
|
-
async invoke(binding, input, threadId, runId, resumePayload, history = []) {
|
|
595
|
+
async invoke(binding, input, threadId, runId, resumePayload, history = [], options = {}) {
|
|
581
596
|
const request = resumePayload === undefined
|
|
582
|
-
?
|
|
597
|
+
? this.buildInvocationRequest(history, input, options)
|
|
583
598
|
: new Command({ resume: resumePayload });
|
|
584
599
|
let result;
|
|
585
600
|
try {
|
|
586
601
|
const runnable = await this.create(binding);
|
|
587
|
-
result = (await this.withTimeout(() => runnable.invoke(request, { configurable: { thread_id: threadId } }), this.resolveBindingTimeout(binding), "agent invoke", "invoke"));
|
|
602
|
+
result = (await this.withTimeout(() => runnable.invoke(request, { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(binding), "agent invoke", "invoke"));
|
|
588
603
|
}
|
|
589
604
|
catch (error) {
|
|
590
605
|
if (resumePayload !== undefined || !isToolCallParseFailure(error)) {
|
|
@@ -592,7 +607,7 @@ export class AgentRuntimeAdapter {
|
|
|
592
607
|
}
|
|
593
608
|
const retriedBinding = this.applyStrictToolJsonInstruction(binding);
|
|
594
609
|
const runnable = await this.create(retriedBinding);
|
|
595
|
-
result = (await this.withTimeout(() => runnable.invoke(
|
|
610
|
+
result = (await this.withTimeout(() => runnable.invoke(this.buildInvocationRequest(history, input, options), { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(retriedBinding), "agent invoke", "invoke"));
|
|
596
611
|
}
|
|
597
612
|
const interruptContent = Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? JSON.stringify(result.__interrupt__) : undefined;
|
|
598
613
|
const extractedOutput = extractVisibleOutput(result);
|
|
@@ -613,7 +628,7 @@ export class AgentRuntimeAdapter {
|
|
|
613
628
|
output: sanitizeVisibleText(output),
|
|
614
629
|
};
|
|
615
630
|
}
|
|
616
|
-
async *stream(binding, input, threadId, history = []) {
|
|
631
|
+
async *stream(binding, input, threadId, history = [], options = {}) {
|
|
617
632
|
try {
|
|
618
633
|
const invokeTimeoutMs = this.resolveBindingTimeout(binding);
|
|
619
634
|
const streamIdleTimeoutMs = this.resolveStreamIdleTimeout(binding);
|
|
@@ -651,9 +666,9 @@ export class AgentRuntimeAdapter {
|
|
|
651
666
|
}
|
|
652
667
|
}
|
|
653
668
|
const runnable = await this.create(binding);
|
|
654
|
-
const request =
|
|
669
|
+
const request = this.buildInvocationRequest(history, input, options);
|
|
655
670
|
if (typeof runnable.streamEvents === "function") {
|
|
656
|
-
const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2" }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
|
|
671
|
+
const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2", ...(options.context ? { context: options.context } : {}) }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
|
|
657
672
|
const allowVisibleStreamDeltas = Boolean(binding.langchainAgentParams);
|
|
658
673
|
let emittedOutput = "";
|
|
659
674
|
let emittedToolError = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { modelCallLimitMiddleware, modelRetryMiddleware, summarizationMiddleware, todoListMiddleware, toolCallLimitMiddleware, toolRetryMiddleware, } from "langchain";
|
|
1
|
+
import { anthropicPromptCachingMiddleware, contextEditingMiddleware, llmToolSelectorMiddleware, modelCallLimitMiddleware, modelFallbackMiddleware, modelRetryMiddleware, openAIModerationMiddleware, summarizationMiddleware, todoListMiddleware, toolCallLimitMiddleware, toolEmulatorMiddleware, toolRetryMiddleware, } from "langchain";
|
|
2
2
|
function asMiddlewareConfig(value) {
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : null;
|
|
4
4
|
}
|
|
@@ -13,6 +13,18 @@ function omitKind(config) {
|
|
|
13
13
|
const { kind: _kind, ...rest } = config;
|
|
14
14
|
return rest;
|
|
15
15
|
}
|
|
16
|
+
async function resolveReferencedModel(value, options) {
|
|
17
|
+
if (value && typeof value === "object" && "id" in value) {
|
|
18
|
+
return options.resolveModel(value);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
async function resolveReferencedModelList(values, options) {
|
|
23
|
+
if (!Array.isArray(values)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return Promise.all(values.map((value) => resolveReferencedModel(value, options)));
|
|
27
|
+
}
|
|
16
28
|
export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
|
|
17
29
|
const resolved = [];
|
|
18
30
|
for (const rawConfig of middlewareConfigs ?? []) {
|
|
@@ -24,18 +36,29 @@ export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
|
|
|
24
36
|
const runtimeConfig = omitKind(config);
|
|
25
37
|
switch (kind) {
|
|
26
38
|
case "summarization": {
|
|
27
|
-
|
|
28
|
-
runtimeConfig.model = await options.resolveModel(runtimeConfig.model);
|
|
29
|
-
}
|
|
39
|
+
runtimeConfig.model = await resolveReferencedModel(runtimeConfig.model, options);
|
|
30
40
|
if (runtimeConfig.model === undefined) {
|
|
31
41
|
throw new Error("summarization middleware requires model or modelRef");
|
|
32
42
|
}
|
|
33
43
|
resolved.push(summarizationMiddleware(runtimeConfig));
|
|
34
44
|
break;
|
|
35
45
|
}
|
|
46
|
+
case "llmToolSelector":
|
|
47
|
+
runtimeConfig.model = await resolveReferencedModel(runtimeConfig.model, options);
|
|
48
|
+
resolved.push(llmToolSelectorMiddleware(runtimeConfig));
|
|
49
|
+
break;
|
|
36
50
|
case "modelRetry":
|
|
37
51
|
resolved.push(modelRetryMiddleware(runtimeConfig));
|
|
38
52
|
break;
|
|
53
|
+
case "modelFallback": {
|
|
54
|
+
const fallbackModels = (await resolveReferencedModelList(runtimeConfig.fallbackModels, options)) ??
|
|
55
|
+
(await resolveReferencedModelList(runtimeConfig.models, options));
|
|
56
|
+
if (!fallbackModels || fallbackModels.length === 0) {
|
|
57
|
+
throw new Error("modelFallback middleware requires fallbackModels or models");
|
|
58
|
+
}
|
|
59
|
+
resolved.push(modelFallbackMiddleware(...fallbackModels));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
39
62
|
case "toolRetry":
|
|
40
63
|
resolved.push(toolRetryMiddleware(runtimeConfig));
|
|
41
64
|
break;
|
|
@@ -48,6 +71,18 @@ export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
|
|
|
48
71
|
case "todoList":
|
|
49
72
|
resolved.push(todoListMiddleware(runtimeConfig));
|
|
50
73
|
break;
|
|
74
|
+
case "contextEditing":
|
|
75
|
+
resolved.push(contextEditingMiddleware(runtimeConfig));
|
|
76
|
+
break;
|
|
77
|
+
case "toolEmulator":
|
|
78
|
+
resolved.push(toolEmulatorMiddleware(runtimeConfig));
|
|
79
|
+
break;
|
|
80
|
+
case "openAIModeration":
|
|
81
|
+
resolved.push(openAIModerationMiddleware(runtimeConfig));
|
|
82
|
+
break;
|
|
83
|
+
case "anthropicPromptCaching":
|
|
84
|
+
resolved.push(anthropicPromptCachingMiddleware(runtimeConfig));
|
|
85
|
+
break;
|
|
51
86
|
default:
|
|
52
87
|
throw new Error(`Unsupported declarative middleware kind ${kind}`);
|
|
53
88
|
}
|
|
@@ -20,6 +20,7 @@ export declare class AgentHarnessRuntime {
|
|
|
20
20
|
private readonly unregisterThreadMemorySync;
|
|
21
21
|
private readonly resolvedRuntimeAdapterOptions;
|
|
22
22
|
private readonly checkpointMaintenance;
|
|
23
|
+
private readonly recoveryConfig;
|
|
23
24
|
private listHostBindings;
|
|
24
25
|
private defaultRunRoot;
|
|
25
26
|
private heuristicRoute;
|
|
@@ -53,8 +54,11 @@ export declare class AgentHarnessRuntime {
|
|
|
53
54
|
private emit;
|
|
54
55
|
private ensureThreadStarted;
|
|
55
56
|
private loadPriorHistory;
|
|
57
|
+
private loadRunInput;
|
|
56
58
|
private appendAssistantMessage;
|
|
57
59
|
private invokeWithHistory;
|
|
60
|
+
private checkpointRefForState;
|
|
61
|
+
private finalizeContinuedRun;
|
|
58
62
|
private emitOutputDeltaAndCreateItem;
|
|
59
63
|
private emitRunCreated;
|
|
60
64
|
private setRunStateAndEmit;
|
|
@@ -73,5 +77,6 @@ export declare class AgentHarnessRuntime {
|
|
|
73
77
|
}>;
|
|
74
78
|
close(): Promise<void>;
|
|
75
79
|
stop(): Promise<void>;
|
|
80
|
+
private recoverStartupRuns;
|
|
76
81
|
}
|
|
77
82
|
export { AgentHarnessRuntime as AgentHarness };
|
package/dist/runtime/harness.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AGENT_INTERRUPT_SENTINEL_PREFIX, AgentRuntimeAdapter, RuntimeOperationT
|
|
|
5
5
|
import { createResourceBackendResolver, createResourceToolResolver } from "../resource/resource.js";
|
|
6
6
|
import { EventBus } from "./event-bus.js";
|
|
7
7
|
import { PolicyEngine } from "./policy-engine.js";
|
|
8
|
-
import { getRoutingDefaultAgentId, getRoutingRules, getRoutingSystemPrompt, isModelRoutingEnabled, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
|
|
8
|
+
import { getRecoveryConfig, getRoutingDefaultAgentId, getRoutingRules, getRoutingSystemPrompt, isModelRoutingEnabled, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
|
|
9
9
|
import { createHarnessEvent, createPendingApproval, heuristicRoute, inferRoutingBindings, renderRuntimeFailure, renderToolFailure, } from "./support/harness-support.js";
|
|
10
10
|
import { createCheckpointerForConfig, createStoreForConfig } from "./support/runtime-factories.js";
|
|
11
11
|
import { resolveCompiledEmbeddingModel, resolveCompiledEmbeddingModelRef } from "./support/embedding-models.js";
|
|
@@ -35,6 +35,7 @@ export class AgentHarnessRuntime {
|
|
|
35
35
|
unregisterThreadMemorySync;
|
|
36
36
|
resolvedRuntimeAdapterOptions;
|
|
37
37
|
checkpointMaintenance;
|
|
38
|
+
recoveryConfig;
|
|
38
39
|
listHostBindings() {
|
|
39
40
|
return inferRoutingBindings(this.workspace).hostBindings;
|
|
40
41
|
}
|
|
@@ -157,10 +158,12 @@ export class AgentHarnessRuntime {
|
|
|
157
158
|
this.checkpointMaintenance = checkpointMaintenanceConfig
|
|
158
159
|
? new CheckpointMaintenanceLoop(discoverCheckpointMaintenanceTargets(workspace), checkpointMaintenanceConfig)
|
|
159
160
|
: null;
|
|
161
|
+
this.recoveryConfig = getRecoveryConfig(workspace.refs);
|
|
160
162
|
}
|
|
161
163
|
async initialize() {
|
|
162
164
|
await this.persistence.initialize();
|
|
163
165
|
await this.checkpointMaintenance?.start();
|
|
166
|
+
await this.recoverStartupRuns();
|
|
164
167
|
}
|
|
165
168
|
subscribe(listener) {
|
|
166
169
|
return this.eventBus.subscribe(listener);
|
|
@@ -338,6 +341,11 @@ export class AgentHarnessRuntime {
|
|
|
338
341
|
const history = await this.persistence.listThreadMessages(threadId);
|
|
339
342
|
return history.filter((message) => message.runId !== runId);
|
|
340
343
|
}
|
|
344
|
+
async loadRunInput(threadId, runId) {
|
|
345
|
+
const history = await this.persistence.listThreadMessages(threadId, 100);
|
|
346
|
+
const userTurn = history.find((message) => message.runId === runId && message.role === "user");
|
|
347
|
+
return userTurn?.content ?? "";
|
|
348
|
+
}
|
|
341
349
|
async appendAssistantMessage(threadId, runId, content) {
|
|
342
350
|
if (!content) {
|
|
343
351
|
return;
|
|
@@ -349,9 +357,31 @@ export class AgentHarnessRuntime {
|
|
|
349
357
|
createdAt: new Date().toISOString(),
|
|
350
358
|
});
|
|
351
359
|
}
|
|
352
|
-
async invokeWithHistory(binding, input, threadId, runId, resumePayload) {
|
|
360
|
+
async invokeWithHistory(binding, input, threadId, runId, resumePayload, options = {}) {
|
|
353
361
|
const priorHistory = await this.loadPriorHistory(threadId, runId);
|
|
354
|
-
return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory);
|
|
362
|
+
return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory, options);
|
|
363
|
+
}
|
|
364
|
+
checkpointRefForState(threadId, runId, state) {
|
|
365
|
+
return state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
|
|
366
|
+
}
|
|
367
|
+
async finalizeContinuedRun(threadId, runId, input, actual, options) {
|
|
368
|
+
let approval;
|
|
369
|
+
await this.appendAssistantMessage(threadId, runId, actual.output);
|
|
370
|
+
const checkpointRef = this.checkpointRefForState(threadId, runId, actual.state);
|
|
371
|
+
await this.setRunStateAndEmit(threadId, runId, options.stateSequence, actual.state, {
|
|
372
|
+
previousState: options.previousState,
|
|
373
|
+
checkpointRef,
|
|
374
|
+
});
|
|
375
|
+
if (actual.state === "waiting_for_approval" && options.approvalSequence) {
|
|
376
|
+
approval = (await this.requestApprovalAndEmit(threadId, runId, input, actual.interruptContent, checkpointRef, options.approvalSequence)).approval;
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
...actual,
|
|
380
|
+
threadId,
|
|
381
|
+
runId,
|
|
382
|
+
approvalId: approval?.approvalId ?? actual.approvalId,
|
|
383
|
+
pendingActionId: approval?.pendingActionId ?? actual.pendingActionId,
|
|
384
|
+
};
|
|
355
385
|
}
|
|
356
386
|
async emitOutputDeltaAndCreateItem(threadId, runId, agentId, content) {
|
|
357
387
|
await this.emit(threadId, runId, 3, "output.delta", {
|
|
@@ -516,24 +546,19 @@ export class AgentHarnessRuntime {
|
|
|
516
546
|
executionMode: binding.agent.executionMode,
|
|
517
547
|
});
|
|
518
548
|
try {
|
|
519
|
-
const actual = await this.invokeWithHistory(binding, options.input, threadId, runId
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
549
|
+
const actual = await this.invokeWithHistory(binding, options.input, threadId, runId, undefined, {
|
|
550
|
+
context: options.context,
|
|
551
|
+
state: options.state,
|
|
552
|
+
files: options.files,
|
|
553
|
+
});
|
|
554
|
+
const finalized = await this.finalizeContinuedRun(threadId, runId, options.input, actual, {
|
|
524
555
|
previousState: null,
|
|
525
|
-
|
|
556
|
+
stateSequence: 3,
|
|
557
|
+
approvalSequence: 4,
|
|
526
558
|
});
|
|
527
|
-
if (actual.state === "waiting_for_approval") {
|
|
528
|
-
approval = (await this.requestApprovalAndEmit(threadId, runId, options.input, actual.interruptContent, checkpointRef, 4)).approval;
|
|
529
|
-
}
|
|
530
559
|
return {
|
|
531
|
-
...
|
|
532
|
-
threadId,
|
|
533
|
-
runId,
|
|
560
|
+
...finalized,
|
|
534
561
|
agentId: selectedAgentId,
|
|
535
|
-
approvalId: approval?.approvalId ?? actual.approvalId,
|
|
536
|
-
pendingActionId: approval?.pendingActionId ?? actual.pendingActionId,
|
|
537
562
|
};
|
|
538
563
|
}
|
|
539
564
|
catch (error) {
|
|
@@ -580,7 +605,11 @@ export class AgentHarnessRuntime {
|
|
|
580
605
|
const priorHistory = await this.loadPriorHistory(threadId, runId);
|
|
581
606
|
let assistantOutput = "";
|
|
582
607
|
const toolErrors = [];
|
|
583
|
-
for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory
|
|
608
|
+
for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory, {
|
|
609
|
+
context: options.context,
|
|
610
|
+
state: options.state,
|
|
611
|
+
files: options.files,
|
|
612
|
+
})) {
|
|
584
613
|
if (chunk) {
|
|
585
614
|
const normalizedChunk = typeof chunk === "string"
|
|
586
615
|
? chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)
|
|
@@ -734,6 +763,15 @@ export class AgentHarnessRuntime {
|
|
|
734
763
|
throw new Error(`Unknown agent ${thread.agentId}`);
|
|
735
764
|
}
|
|
736
765
|
await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
|
|
766
|
+
await this.persistence.saveRecoveryIntent(threadId, runId, {
|
|
767
|
+
kind: "approval-decision",
|
|
768
|
+
savedAt: new Date().toISOString(),
|
|
769
|
+
checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
|
|
770
|
+
resumePayload: options.decision === "edit" && options.editedInput
|
|
771
|
+
? { decision: "edit", editedInput: options.editedInput }
|
|
772
|
+
: (options.decision ?? "approve"),
|
|
773
|
+
attempts: 0,
|
|
774
|
+
});
|
|
737
775
|
await this.emit(threadId, runId, 5, "run.resumed", {
|
|
738
776
|
resumeKind: "cross-restart",
|
|
739
777
|
checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
|
|
@@ -750,24 +788,27 @@ export class AgentHarnessRuntime {
|
|
|
750
788
|
});
|
|
751
789
|
const history = await this.persistence.listThreadMessages(threadId);
|
|
752
790
|
const priorHistory = history.filter((message) => message.runId !== runId);
|
|
791
|
+
const runInput = await this.loadRunInput(threadId, runId);
|
|
753
792
|
const resumeDecision = options.decision === "edit" && options.editedInput
|
|
754
793
|
? { decision: "edit", editedInput: options.editedInput }
|
|
755
794
|
: (options.decision ?? "approve");
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
795
|
+
try {
|
|
796
|
+
const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
|
|
797
|
+
await this.persistence.clearRecoveryIntent(threadId, runId);
|
|
798
|
+
const finalized = await this.finalizeContinuedRun(threadId, runId, runInput, actual, {
|
|
799
|
+
previousState: "resuming",
|
|
800
|
+
stateSequence: 7,
|
|
801
|
+
approvalSequence: 8,
|
|
802
|
+
});
|
|
803
|
+
return {
|
|
804
|
+
...finalized,
|
|
805
|
+
approvalId: finalized.approvalId ?? approval.approvalId,
|
|
806
|
+
pendingActionId: finalized.pendingActionId ?? approval.pendingActionId,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
771
812
|
}
|
|
772
813
|
async restartConversation(options) {
|
|
773
814
|
const thread = await this.getSession(options.threadId);
|
|
@@ -802,5 +843,62 @@ export class AgentHarnessRuntime {
|
|
|
802
843
|
async stop() {
|
|
803
844
|
await this.close();
|
|
804
845
|
}
|
|
846
|
+
async recoverStartupRuns() {
|
|
847
|
+
if (!this.recoveryConfig.enabled || !this.recoveryConfig.resumeResumingRunsOnStartup) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const threads = await this.persistence.listSessions();
|
|
851
|
+
for (const thread of threads) {
|
|
852
|
+
if (thread.status !== "resuming") {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const binding = this.workspace.bindings.get(thread.agentId);
|
|
856
|
+
if (!binding) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const recoveryIntent = await this.persistence.getRecoveryIntent(thread.threadId, thread.latestRunId);
|
|
860
|
+
if (!recoveryIntent || recoveryIntent.kind !== "approval-decision") {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (recoveryIntent.attempts >= this.recoveryConfig.maxRecoveryAttempts) {
|
|
864
|
+
await this.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
|
|
865
|
+
await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
await this.persistence.saveRecoveryIntent(thread.threadId, thread.latestRunId, {
|
|
869
|
+
...recoveryIntent,
|
|
870
|
+
attempts: recoveryIntent.attempts + 1,
|
|
871
|
+
});
|
|
872
|
+
await this.emit(thread.threadId, thread.latestRunId, 100, "run.resumed", {
|
|
873
|
+
resumeKind: "startup-recovery",
|
|
874
|
+
checkpointRef: recoveryIntent.checkpointRef,
|
|
875
|
+
state: "resuming",
|
|
876
|
+
});
|
|
877
|
+
const history = await this.persistence.listThreadMessages(thread.threadId);
|
|
878
|
+
const priorHistory = history.filter((message) => message.runId !== thread.latestRunId);
|
|
879
|
+
const runInput = await this.loadRunInput(thread.threadId, thread.latestRunId);
|
|
880
|
+
try {
|
|
881
|
+
const actual = await this.runtimeAdapter.invoke(binding, "", thread.threadId, thread.latestRunId, recoveryIntent.resumePayload, priorHistory);
|
|
882
|
+
await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
|
|
883
|
+
await this.finalizeContinuedRun(thread.threadId, thread.latestRunId, runInput, actual, {
|
|
884
|
+
previousState: "resuming",
|
|
885
|
+
stateSequence: 101,
|
|
886
|
+
approvalSequence: 102,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
if (recoveryIntent.attempts + 1 >= this.recoveryConfig.maxRecoveryAttempts) {
|
|
891
|
+
await this.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
|
|
892
|
+
await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
|
|
893
|
+
await this.emit(thread.threadId, thread.latestRunId, 101, "run.state.changed", {
|
|
894
|
+
previousState: "resuming",
|
|
895
|
+
state: "failed",
|
|
896
|
+
checkpointRef: recoveryIntent.checkpointRef,
|
|
897
|
+
error: error instanceof Error ? error.message : String(error),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
805
903
|
}
|
|
806
904
|
export { AgentHarnessRuntime as AgentHarness };
|
|
@@ -43,6 +43,22 @@ function compileMiddlewareConfigs(middleware, models, ownerId) {
|
|
|
43
43
|
compiled.model = requireModel(models, compiled.modelRef, ownerId);
|
|
44
44
|
delete compiled.modelRef;
|
|
45
45
|
}
|
|
46
|
+
if (compiled.kind === "llmToolSelector" && typeof compiled.modelRef === "string") {
|
|
47
|
+
compiled.model = requireModel(models, compiled.modelRef, ownerId);
|
|
48
|
+
delete compiled.modelRef;
|
|
49
|
+
}
|
|
50
|
+
if (compiled.kind === "modelFallback") {
|
|
51
|
+
const fallbackModelRefs = Array.isArray(compiled.fallbackModelRefs)
|
|
52
|
+
? compiled.fallbackModelRefs
|
|
53
|
+
: Array.isArray(compiled.modelRefs)
|
|
54
|
+
? compiled.modelRefs
|
|
55
|
+
: [];
|
|
56
|
+
if (fallbackModelRefs.length > 0) {
|
|
57
|
+
compiled.fallbackModels = fallbackModelRefs.map((modelRef) => requireModel(models, String(modelRef), ownerId));
|
|
58
|
+
delete compiled.fallbackModelRefs;
|
|
59
|
+
delete compiled.modelRefs;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
return compiled;
|
|
47
63
|
});
|
|
48
64
|
}
|
|
@@ -162,9 +178,15 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
|
|
|
162
178
|
model: compiledAgentModel,
|
|
163
179
|
tools: requireTools(tools, agent.toolRefs, agent.id),
|
|
164
180
|
systemPrompt: resolveSystemPrompt(agent),
|
|
181
|
+
stateSchema: agent.langchainAgentConfig?.stateSchema,
|
|
165
182
|
responseFormat: agent.langchainAgentConfig?.responseFormat,
|
|
166
183
|
contextSchema: agent.langchainAgentConfig?.contextSchema,
|
|
167
184
|
middleware: compileMiddlewareConfigs(agent.langchainAgentConfig?.middleware, models, agent.id),
|
|
185
|
+
includeAgentName: agent.langchainAgentConfig?.includeAgentName === "inline" ? "inline" : undefined,
|
|
186
|
+
version: agent.langchainAgentConfig?.version === "v1" || agent.langchainAgentConfig?.version === "v2"
|
|
187
|
+
? agent.langchainAgentConfig.version
|
|
188
|
+
: undefined,
|
|
189
|
+
name: resolveAgentRuntimeName(agent),
|
|
168
190
|
description: agent.description,
|
|
169
191
|
};
|
|
170
192
|
return {
|
|
@@ -196,6 +218,10 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
|
|
|
196
218
|
name: resolveAgentRuntimeName(agent),
|
|
197
219
|
memory: compiledAgentMemory,
|
|
198
220
|
skills: compiledAgentSkills,
|
|
221
|
+
generalPurposeAgent: typeof agent.deepAgentConfig?.generalPurposeAgent === "boolean" ? agent.deepAgentConfig.generalPurposeAgent : undefined,
|
|
222
|
+
taskDescription: typeof agent.deepAgentConfig?.taskDescription === "string" && agent.deepAgentConfig.taskDescription.trim()
|
|
223
|
+
? agent.deepAgentConfig.taskDescription
|
|
224
|
+
: undefined,
|
|
199
225
|
},
|
|
200
226
|
};
|
|
201
227
|
}
|
|
@@ -5,7 +5,7 @@ import { validateAgent, validateTopology } from "./validate.js";
|
|
|
5
5
|
import { compileBinding } from "./agent-binding-compiler.js";
|
|
6
6
|
import { discoverSubagents, ensureDiscoverySources } from "./support/discovery.js";
|
|
7
7
|
import { collectAgentDiscoverySourceRefs, collectToolSourceRefs } from "./support/source-collectors.js";
|
|
8
|
-
import { resolveRefId } from "./support/workspace-ref-utils.js";
|
|
8
|
+
import { getRoutingDefaultAgentId, getRoutingRules, resolveRefId, } from "./support/workspace-ref-utils.js";
|
|
9
9
|
import { hydrateAgentMcpTools, hydrateResourceAndExternalTools } from "./tool-hydration.js";
|
|
10
10
|
function collectParsedResources(refs) {
|
|
11
11
|
const embeddings = new Map();
|
|
@@ -70,6 +70,20 @@ function compileBindings(workspaceRoot, refs, agentsList, models, tools) {
|
|
|
70
70
|
}
|
|
71
71
|
return bindings;
|
|
72
72
|
}
|
|
73
|
+
function validateRoutingTargets(refs, agentsList) {
|
|
74
|
+
const hostFacingAgentIds = new Set(agentsList
|
|
75
|
+
.filter((agent) => !agentsList.some((owner) => owner.subagentRefs.some((ref) => resolveRefId(ref) === agent.id)))
|
|
76
|
+
.map((agent) => agent.id));
|
|
77
|
+
const defaultAgentId = getRoutingDefaultAgentId(refs);
|
|
78
|
+
if (defaultAgentId && !hostFacingAgentIds.has(defaultAgentId)) {
|
|
79
|
+
throw new Error(`Runtime routing.defaultAgentId references unknown host-facing agent ${defaultAgentId}`);
|
|
80
|
+
}
|
|
81
|
+
for (const rule of getRoutingRules(refs)) {
|
|
82
|
+
if (!hostFacingAgentIds.has(rule.agentId)) {
|
|
83
|
+
throw new Error(`Runtime routing.rules references unknown host-facing agent ${rule.agentId}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
73
87
|
export async function loadWorkspace(workspaceRoot, options = {}) {
|
|
74
88
|
const loaded = await loadWorkspaceObjects(workspaceRoot, options);
|
|
75
89
|
loaded.agents = await discoverSubagents(loaded.agents, workspaceRoot);
|
|
@@ -84,6 +98,7 @@ export async function loadWorkspace(workspaceRoot, options = {}) {
|
|
|
84
98
|
await ensureResourceSources(toolSourceRefs, workspaceRoot);
|
|
85
99
|
await hydrateResourceAndExternalTools(tools, toolSourceRefs, workspaceRoot);
|
|
86
100
|
validateWorkspaceResources(embeddings, mcpServers, models, vectorStores, tools, loaded.agents);
|
|
101
|
+
validateRoutingTargets(loaded.refs, loaded.agents);
|
|
87
102
|
return {
|
|
88
103
|
workspaceRoot,
|
|
89
104
|
resourceSources: [...toolSourceRefs],
|
|
@@ -215,8 +215,11 @@ function readSharedAgentConfig(item) {
|
|
|
215
215
|
...(typeof item.systemPrompt === "string" ? { systemPrompt: item.systemPrompt } : {}),
|
|
216
216
|
...(typeof item.checkpointer === "object" && item.checkpointer ? { checkpointer: item.checkpointer } : {}),
|
|
217
217
|
...(typeof item.interruptOn === "object" && item.interruptOn ? { interruptOn: item.interruptOn } : {}),
|
|
218
|
+
...(item.stateSchema !== undefined ? { stateSchema: item.stateSchema } : {}),
|
|
218
219
|
...(item.responseFormat !== undefined ? { responseFormat: item.responseFormat } : {}),
|
|
219
220
|
...(item.contextSchema !== undefined ? { contextSchema: item.contextSchema } : {}),
|
|
221
|
+
...(item.includeAgentName === "inline" ? { includeAgentName: "inline" } : {}),
|
|
222
|
+
...(item.version === "v1" || item.version === "v2" ? { version: item.version } : {}),
|
|
220
223
|
...(middleware ? { middleware } : {}),
|
|
221
224
|
};
|
|
222
225
|
}
|
|
@@ -228,6 +231,8 @@ function readDeepAgentConfig(item) {
|
|
|
228
231
|
...readSharedAgentConfig(item),
|
|
229
232
|
...(typeof item.backend === "object" && item.backend ? { backend: item.backend } : {}),
|
|
230
233
|
...(typeof item.store === "object" && item.store ? { store: item.store } : {}),
|
|
234
|
+
...(typeof item.taskDescription === "string" && item.taskDescription.trim() ? { taskDescription: item.taskDescription } : {}),
|
|
235
|
+
...(typeof item.generalPurposeAgent === "boolean" ? { generalPurposeAgent: item.generalPurposeAgent } : {}),
|
|
231
236
|
};
|
|
232
237
|
}
|
|
233
238
|
export function parseAgentItem(item, sourcePath) {
|
|
@@ -12,8 +12,19 @@ export type RoutingRule = {
|
|
|
12
12
|
hasThreadId?: boolean;
|
|
13
13
|
caseSensitive?: boolean;
|
|
14
14
|
};
|
|
15
|
+
export type RuntimeRecoveryConfig = {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
resumeOnStartup: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type RecoveryConfig = {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
resumeResumingRunsOnStartup: boolean;
|
|
22
|
+
maxRecoveryAttempts: number;
|
|
23
|
+
};
|
|
15
24
|
export declare function getWorkspaceObject(refs: Map<string, WorkspaceObject | ParsedAgentObject>, ref: string | undefined): WorkspaceObject | undefined;
|
|
16
25
|
export declare function getRuntimeDefaults(refs: Map<string, WorkspaceObject | ParsedAgentObject>): Record<string, unknown> | undefined;
|
|
26
|
+
export declare function getRuntimeRecoveryConfig(refs: Map<string, WorkspaceObject | ParsedAgentObject>): RuntimeRecoveryConfig;
|
|
27
|
+
export declare function getRecoveryConfig(refs: Map<string, WorkspaceObject | ParsedAgentObject>): RecoveryConfig;
|
|
17
28
|
export declare function getRoutingSystemPrompt(refs: Map<string, WorkspaceObject | ParsedAgentObject>): string | undefined;
|
|
18
29
|
export declare function getRoutingDefaultAgentId(refs: Map<string, WorkspaceObject | ParsedAgentObject>): string | undefined;
|
|
19
30
|
export declare function isModelRoutingEnabled(refs: Map<string, WorkspaceObject | ParsedAgentObject>): boolean;
|
|
@@ -25,6 +25,32 @@ export function getRuntimeDefaults(refs) {
|
|
|
25
25
|
}
|
|
26
26
|
return runtimes[0].value;
|
|
27
27
|
}
|
|
28
|
+
export function getRuntimeRecoveryConfig(refs) {
|
|
29
|
+
const runtimeDefaults = getRuntimeDefaults(refs);
|
|
30
|
+
const recovery = typeof runtimeDefaults?.recovery === "object" && runtimeDefaults.recovery
|
|
31
|
+
? runtimeDefaults.recovery
|
|
32
|
+
: undefined;
|
|
33
|
+
return {
|
|
34
|
+
enabled: recovery?.enabled !== false,
|
|
35
|
+
resumeOnStartup: recovery?.resumeOnStartup !== false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function getRecoveryConfig(refs) {
|
|
39
|
+
const runtimeDefaults = getRuntimeDefaults(refs);
|
|
40
|
+
const recovery = typeof runtimeDefaults?.recovery === "object" && runtimeDefaults.recovery
|
|
41
|
+
? runtimeDefaults.recovery
|
|
42
|
+
: {};
|
|
43
|
+
const maxRecoveryAttempts = typeof recovery.maxRecoveryAttempts === "number" &&
|
|
44
|
+
Number.isFinite(recovery.maxRecoveryAttempts) &&
|
|
45
|
+
recovery.maxRecoveryAttempts > 0
|
|
46
|
+
? Math.floor(recovery.maxRecoveryAttempts)
|
|
47
|
+
: 3;
|
|
48
|
+
return {
|
|
49
|
+
enabled: recovery.enabled !== false,
|
|
50
|
+
resumeResumingRunsOnStartup: recovery.resumeResumingRunsOnStartup !== false,
|
|
51
|
+
maxRecoveryAttempts,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
28
54
|
export function getRoutingSystemPrompt(refs) {
|
|
29
55
|
const routing = getRoutingObject(refs);
|
|
30
56
|
return typeof routing?.systemPrompt === "string" && routing.systemPrompt.trim() ? routing.systemPrompt : undefined;
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
const allowedExecutionModes = new Set(["deepagent", "langchain-v1"]);
|
|
2
|
-
const allowedMiddlewareKinds = new Set([
|
|
3
|
-
"summarization",
|
|
4
|
-
"modelRetry",
|
|
5
|
-
"toolRetry",
|
|
6
|
-
"toolCallLimit",
|
|
7
|
-
"modelCallLimit",
|
|
8
|
-
"todoList",
|
|
9
|
-
]);
|
|
10
2
|
function hasPromptContent(value) {
|
|
11
3
|
return typeof value === "string" && value.trim().length > 0;
|
|
12
4
|
}
|
|
@@ -33,13 +25,16 @@ function validateMiddlewareConfig(agent) {
|
|
|
33
25
|
throw new Error(`Agent ${agent.id} middleware entries must be objects`);
|
|
34
26
|
}
|
|
35
27
|
const typed = config;
|
|
36
|
-
const kind = typeof typed.kind === "string" ? typed.kind : "";
|
|
37
|
-
if (!
|
|
38
|
-
throw new Error(`Agent ${agent.id} middleware kind
|
|
28
|
+
const kind = typeof typed.kind === "string" ? typed.kind.trim() : "";
|
|
29
|
+
if (!kind) {
|
|
30
|
+
throw new Error(`Agent ${agent.id} middleware kind is required`);
|
|
39
31
|
}
|
|
40
32
|
if (kind === "summarization" && typed.model === undefined && typeof typed.modelRef !== "string") {
|
|
41
33
|
throw new Error(`Agent ${agent.id} summarization middleware requires model or modelRef`);
|
|
42
34
|
}
|
|
35
|
+
if (kind === "modelFallback" && !Array.isArray(typed.fallbackModels) && !Array.isArray(typed.models)) {
|
|
36
|
+
throw new Error(`Agent ${agent.id} modelFallback middleware requires fallbackModels or models`);
|
|
37
|
+
}
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
40
|
export function validateAgent(agent) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botbotgo/agent-harness",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.41",
|
|
4
4
|
"description": "Workspace runtime for multi-agent applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
|
|
52
52
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
53
|
-
"test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/runtime-adapter-regressions.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts",
|
|
53
|
+
"test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/runtime-adapter-regressions.test.ts test/runtime-recovery.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts",
|
|
54
54
|
"test:real-providers": "vitest run test/real-provider-harness.test.ts",
|
|
55
55
|
"release:prepare": "npm version patch --no-git-tag-version && node ./scripts/sync-example-version.mjs",
|
|
56
56
|
"release:pack": "npm pack --dry-run",
|