@agentforge-io/core 2.0.8 → 2.0.10
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/services/agent-runner.service.d.ts +16 -0
- package/dist/services/agent-runner.service.js +43 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +10 -1
- package/dist/services/tool-approval-gate.d.ts +97 -0
- package/dist/services/tool-approval-gate.js +58 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import type { AgentOverrides, AgentResponse, AnthropicMessage, StreamChunk, Tool
|
|
|
2
2
|
import type { AgentDefinition, AnthropicConfig } from '../types/config.types';
|
|
3
3
|
import type { ToolRegistryService } from './tool-registry.service';
|
|
4
4
|
import type { Logger } from './tool-registry.service';
|
|
5
|
+
import { type ToolApprovalGate } from './tool-approval-gate';
|
|
5
6
|
/**
|
|
6
7
|
* Framework-free runner for Claude. Handles the agentic loop (tool calls) for
|
|
7
8
|
* sync runs and exposes streaming as an `AsyncGenerator<StreamChunk>` so any
|
|
@@ -12,8 +13,23 @@ export declare class AgentRunnerService {
|
|
|
12
13
|
private readonly toolRegistry;
|
|
13
14
|
private readonly client;
|
|
14
15
|
private readonly logger;
|
|
16
|
+
/**
|
|
17
|
+
* Optional pre-dispatch gate. When supplied, every tool call passes
|
|
18
|
+
* through `check()` before the actual handler runs. The gate may:
|
|
19
|
+
* - return `allow` → execution proceeds (identical to no-gate path)
|
|
20
|
+
* - return `blocked` → runner throws ToolBlockedError; loop bubbles up
|
|
21
|
+
* - return `approval` → runner throws ToolApprovalRequired; the
|
|
22
|
+
* consumer is responsible for catching, persisting state, and
|
|
23
|
+
* resuming later (typically by re-running the turn after the
|
|
24
|
+
* human approves).
|
|
25
|
+
*
|
|
26
|
+
* Left undefined by default to keep deployments without an approval
|
|
27
|
+
* story behaviorally identical to the pre-gate codebase.
|
|
28
|
+
*/
|
|
29
|
+
private readonly approvalGate;
|
|
15
30
|
constructor(anthropicConfig: AnthropicConfig, toolRegistry: ToolRegistryService, opts?: {
|
|
16
31
|
logger?: Logger;
|
|
32
|
+
approvalGate?: ToolApprovalGate;
|
|
17
33
|
});
|
|
18
34
|
run(agent: AgentDefinition, messages: AnthropicMessage[], context: ToolExecutionContext, overrides?: AgentOverrides): Promise<AgentResponse>;
|
|
19
35
|
stream(agent: AgentDefinition, messages: AnthropicMessage[], context: ToolExecutionContext, overrides?: AgentOverrides): AsyncGenerator<StreamChunk>;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.AgentRunnerService = void 0;
|
|
7
7
|
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
8
8
|
const crypto_1 = require("crypto");
|
|
9
|
+
const tool_approval_gate_1 = require("./tool-approval-gate");
|
|
9
10
|
const noopLogger = {
|
|
10
11
|
log: () => { }, warn: () => { }, debug: () => { }, error: () => { },
|
|
11
12
|
};
|
|
@@ -23,6 +24,7 @@ class AgentRunnerService {
|
|
|
23
24
|
baseURL: anthropicConfig.baseURL,
|
|
24
25
|
});
|
|
25
26
|
this.logger = opts.logger ?? noopLogger;
|
|
27
|
+
this.approvalGate = opts.approvalGate;
|
|
26
28
|
}
|
|
27
29
|
// ─── Run (non-streaming) ──────────────────────────────────────────────────
|
|
28
30
|
async run(agent, messages, context, overrides) {
|
|
@@ -68,6 +70,17 @@ class AgentRunnerService {
|
|
|
68
70
|
output = await this.dispatchTool(block.name, block.input, context, extras);
|
|
69
71
|
}
|
|
70
72
|
catch (err) {
|
|
73
|
+
// Approval-gate signals are NOT tool execution errors —
|
|
74
|
+
// they ARE the surface the caller of run() branches on.
|
|
75
|
+
// Re-throw so the loop aborts and the consumer (executor /
|
|
76
|
+
// conversation service) can persist the pause + surface
|
|
77
|
+
// the approvalId. Without this re-throw, the runner would
|
|
78
|
+
// feed back a `"Error executing tool …"` to the LLM,
|
|
79
|
+
// hiding the pause behind a regular tool failure.
|
|
80
|
+
if (err instanceof tool_approval_gate_1.ToolApprovalRequired ||
|
|
81
|
+
err instanceof tool_approval_gate_1.ToolBlockedError) {
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
71
84
|
error = err instanceof Error ? err.message : String(err);
|
|
72
85
|
output = `Error executing tool ${block.name}: ${error}`;
|
|
73
86
|
}
|
|
@@ -165,6 +178,15 @@ class AgentRunnerService {
|
|
|
165
178
|
output = await this.dispatchTool(block.name, block.input, context, extras);
|
|
166
179
|
}
|
|
167
180
|
catch (err) {
|
|
181
|
+
// Same rule as the sync path: gate signals propagate, regular
|
|
182
|
+
// errors collapse into a `"Error: …"` tool result the LLM
|
|
183
|
+
// can react to. The consumer of `stream()` catches the
|
|
184
|
+
// ToolApprovalRequired and decides whether to emit a
|
|
185
|
+
// structured chunk to the client or just end the stream.
|
|
186
|
+
if (err instanceof tool_approval_gate_1.ToolApprovalRequired ||
|
|
187
|
+
err instanceof tool_approval_gate_1.ToolBlockedError) {
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
168
190
|
output = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
169
191
|
}
|
|
170
192
|
yield { type: 'tool_result', toolName: block.name, result: output };
|
|
@@ -261,6 +283,27 @@ class AgentRunnerService {
|
|
|
261
283
|
return { tools: tools.length ? tools : undefined, extras };
|
|
262
284
|
}
|
|
263
285
|
async dispatchTool(name, input, context, extras) {
|
|
286
|
+
// Consult the approval gate before any tool work happens. The throws
|
|
287
|
+
// here are caught and re-raised verbatim by `run()` / `stream()` —
|
|
288
|
+
// they ARE the surface the consumer branches on, not generic errors.
|
|
289
|
+
if (this.approvalGate) {
|
|
290
|
+
const decision = await this.approvalGate.check({
|
|
291
|
+
toolName: name,
|
|
292
|
+
toolInput: input,
|
|
293
|
+
context,
|
|
294
|
+
});
|
|
295
|
+
if (decision.kind === 'blocked') {
|
|
296
|
+
throw new tool_approval_gate_1.ToolBlockedError(name, decision.reason);
|
|
297
|
+
}
|
|
298
|
+
if (decision.kind === 'approval') {
|
|
299
|
+
throw new tool_approval_gate_1.ToolApprovalRequired({
|
|
300
|
+
approvalId: decision.approvalId,
|
|
301
|
+
toolName: name,
|
|
302
|
+
expiresAt: decision.expiresAt,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// `allow` falls through to the normal dispatch path.
|
|
306
|
+
}
|
|
264
307
|
const extra = extras.get(name);
|
|
265
308
|
if (extra)
|
|
266
309
|
return extra.execute(input, context);
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { ToolRegistryService, type Logger, type ToolDescription, } from './tool-registry.service';
|
|
2
2
|
export { AgentRunnerService } from './agent-runner.service';
|
|
3
|
+
export { ToolApprovalRequired, ToolBlockedError, isToolApprovalRequired, isToolBlockedError, type ToolApprovalGate, type ToolApprovalCheckArgs, type ToolApprovalDecision, } from './tool-approval-gate';
|
|
3
4
|
export { PreparedStreamService, PreparedStreamError, } from './prepared-stream.service';
|
|
4
5
|
export { InMemoryPreparedStreamStore } from './in-memory-prepared-stream.store';
|
|
5
6
|
export { ConversationService, ConversationNotFoundError, } from './conversation.service';
|
package/dist/services/index.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InMemoryAuthorizeStateStore = exports.ConnectorError = exports.ConnectorRegistryService = exports.OAuth2Service = exports.McpServerError = exports.McpServerService = exports.McpClientError = exports.McpClientService = exports.ChatTokenError = exports.ChatTokenService = exports.AgentJobWorker = exports.OrchestratorError = exports.OrchestratorService = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.InMemoryPreparedStreamStore = exports.PreparedStreamError = exports.PreparedStreamService = exports.AgentRunnerService = exports.ToolRegistryService = void 0;
|
|
3
|
+
exports.InMemoryAuthorizeStateStore = exports.ConnectorError = exports.ConnectorRegistryService = exports.OAuth2Service = exports.McpServerError = exports.McpServerService = exports.McpClientError = exports.McpClientService = exports.ChatTokenError = exports.ChatTokenService = exports.AgentJobWorker = exports.OrchestratorError = exports.OrchestratorService = exports.AgentForbiddenError = exports.AgentService = exports.ConversationNotFoundError = exports.ConversationService = exports.InMemoryPreparedStreamStore = exports.PreparedStreamError = exports.PreparedStreamService = exports.isToolBlockedError = exports.isToolApprovalRequired = exports.ToolBlockedError = exports.ToolApprovalRequired = exports.AgentRunnerService = exports.ToolRegistryService = void 0;
|
|
4
4
|
// Public service surface of @agentforge-io/core. AI runtime only — auth,
|
|
5
5
|
// identity, billing, infra, etc. have all moved to the host server.
|
|
6
6
|
var tool_registry_service_1 = require("./tool-registry.service");
|
|
7
7
|
Object.defineProperty(exports, "ToolRegistryService", { enumerable: true, get: function () { return tool_registry_service_1.ToolRegistryService; } });
|
|
8
8
|
var agent_runner_service_1 = require("./agent-runner.service");
|
|
9
9
|
Object.defineProperty(exports, "AgentRunnerService", { enumerable: true, get: function () { return agent_runner_service_1.AgentRunnerService; } });
|
|
10
|
+
// Approval gate primitives. Optional dependency of the runner — hosts
|
|
11
|
+
// that don't need human-in-the-loop tool calls leave it unwired and
|
|
12
|
+
// nothing changes. See @agentforge-io/server's ApprovalGateAdapter for
|
|
13
|
+
// a reference impl.
|
|
14
|
+
var tool_approval_gate_1 = require("./tool-approval-gate");
|
|
15
|
+
Object.defineProperty(exports, "ToolApprovalRequired", { enumerable: true, get: function () { return tool_approval_gate_1.ToolApprovalRequired; } });
|
|
16
|
+
Object.defineProperty(exports, "ToolBlockedError", { enumerable: true, get: function () { return tool_approval_gate_1.ToolBlockedError; } });
|
|
17
|
+
Object.defineProperty(exports, "isToolApprovalRequired", { enumerable: true, get: function () { return tool_approval_gate_1.isToolApprovalRequired; } });
|
|
18
|
+
Object.defineProperty(exports, "isToolBlockedError", { enumerable: true, get: function () { return tool_approval_gate_1.isToolBlockedError; } });
|
|
10
19
|
var prepared_stream_service_1 = require("./prepared-stream.service");
|
|
11
20
|
Object.defineProperty(exports, "PreparedStreamService", { enumerable: true, get: function () { return prepared_stream_service_1.PreparedStreamService; } });
|
|
12
21
|
Object.defineProperty(exports, "PreparedStreamError", { enumerable: true, get: function () { return prepared_stream_service_1.PreparedStreamError; } });
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ToolExecutionContext } from '../types/agent.types';
|
|
2
|
+
/**
|
|
3
|
+
* Optional pre-dispatch gate consulted by `AgentRunnerService` before
|
|
4
|
+
* every tool call. The runner is intentionally agnostic about WHERE the
|
|
5
|
+
* decision comes from — the host (e.g. AgentForge platform) supplies a
|
|
6
|
+
* concrete implementation backed by its own DB / config.
|
|
7
|
+
*
|
|
8
|
+
* Three possible decisions:
|
|
9
|
+
*
|
|
10
|
+
* - `allow` → runner proceeds to invoke the tool. Identical to the
|
|
11
|
+
* pre-gate behavior, so a deployment without a gate
|
|
12
|
+
* installed is a no-op.
|
|
13
|
+
* - `blocked` → runner throws `ToolBlockedError`. In a healthy
|
|
14
|
+
* deployment a blocked tool isn't even injected into
|
|
15
|
+
* the LLM's tool list, so this branch is defense-in-
|
|
16
|
+
* depth against a stale agent definition the model
|
|
17
|
+
* somehow still references.
|
|
18
|
+
* - `approval` → runner throws `ToolApprovalRequired`. The caller of
|
|
19
|
+
* `run()` / `stream()` is expected to catch this,
|
|
20
|
+
* persist whatever it needs to resume later, and
|
|
21
|
+
* surface the approval id to the end consumer.
|
|
22
|
+
*
|
|
23
|
+
* The async signature is deliberate — every realistic adapter will need
|
|
24
|
+
* at least one DB read (look up the tool's mode on the agent) and often
|
|
25
|
+
* one write (open the approval row).
|
|
26
|
+
*/
|
|
27
|
+
export interface ToolApprovalGate {
|
|
28
|
+
check(args: ToolApprovalCheckArgs): Promise<ToolApprovalDecision>;
|
|
29
|
+
}
|
|
30
|
+
export interface ToolApprovalCheckArgs {
|
|
31
|
+
/** The tool name as the LLM emitted it (post-prefix for connector +
|
|
32
|
+
* MCP tools — same string the runner uses to look the tool up in
|
|
33
|
+
* the registry). */
|
|
34
|
+
toolName: string;
|
|
35
|
+
/** Tool arguments the LLM produced. The gate may persist these on a
|
|
36
|
+
* pending approval row so a human can inspect; the runner never
|
|
37
|
+
* mutates them after the gate returns. */
|
|
38
|
+
toolInput: Record<string, unknown>;
|
|
39
|
+
/** Same context object passed to the tool's `execute`. Carries the
|
|
40
|
+
* agent / conversation / user identities the adapter needs to scope
|
|
41
|
+
* its lookups. */
|
|
42
|
+
context: ToolExecutionContext;
|
|
43
|
+
}
|
|
44
|
+
export type ToolApprovalDecision = {
|
|
45
|
+
kind: 'allow';
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'blocked';
|
|
48
|
+
reason?: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'approval';
|
|
51
|
+
/** Opaque id of the approval row the gate just created. The
|
|
52
|
+
* consumer surfaces this to the operator so they can find the
|
|
53
|
+
* row in the approvals queue. */
|
|
54
|
+
approvalId: string;
|
|
55
|
+
/** ISO timestamp at which the approval will auto-expire. Useful
|
|
56
|
+
* for the consumer to set a comparable timeout on its own
|
|
57
|
+
* side (e.g. expire a paused conversation). */
|
|
58
|
+
expiresAt: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Thrown by the runner when the gate returns `approval`. The caller is
|
|
62
|
+
* expected to catch this exact class (not a plain `Error`) and translate
|
|
63
|
+
* it to its surface: an automation run row flips to `awaiting_approval`,
|
|
64
|
+
* a conversation appends a synthetic message, a public chat stream
|
|
65
|
+
* emits a structured chunk, etc.
|
|
66
|
+
*
|
|
67
|
+
* Carries the approval id + expiry so the surface can echo them back to
|
|
68
|
+
* the consumer without an extra DB read.
|
|
69
|
+
*/
|
|
70
|
+
export declare class ToolApprovalRequired extends Error {
|
|
71
|
+
readonly approvalId: string;
|
|
72
|
+
readonly toolName: string;
|
|
73
|
+
readonly expiresAt: string;
|
|
74
|
+
constructor(args: {
|
|
75
|
+
approvalId: string;
|
|
76
|
+
toolName: string;
|
|
77
|
+
expiresAt: string;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Thrown by the runner when the gate returns `blocked`. Different class
|
|
82
|
+
* than `ToolApprovalRequired` so callers can branch — `awaiting_approval`
|
|
83
|
+
* is a pause, `blocked` is a hard refusal.
|
|
84
|
+
*/
|
|
85
|
+
export declare class ToolBlockedError extends Error {
|
|
86
|
+
readonly toolName: string;
|
|
87
|
+
readonly reason?: string;
|
|
88
|
+
constructor(toolName: string, reason?: string);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Cheap helpers for catch-blocks that want to know whether a thrown
|
|
92
|
+
* error escaped from the gate path. Using these avoids `instanceof`
|
|
93
|
+
* realm pitfalls when the lib's compiled code is imported by both the
|
|
94
|
+
* platform's source tree and a vendored copy in tests.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isToolApprovalRequired(err: unknown): err is ToolApprovalRequired;
|
|
97
|
+
export declare function isToolBlockedError(err: unknown): err is ToolBlockedError;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolBlockedError = exports.ToolApprovalRequired = void 0;
|
|
4
|
+
exports.isToolApprovalRequired = isToolApprovalRequired;
|
|
5
|
+
exports.isToolBlockedError = isToolBlockedError;
|
|
6
|
+
/**
|
|
7
|
+
* Thrown by the runner when the gate returns `approval`. The caller is
|
|
8
|
+
* expected to catch this exact class (not a plain `Error`) and translate
|
|
9
|
+
* it to its surface: an automation run row flips to `awaiting_approval`,
|
|
10
|
+
* a conversation appends a synthetic message, a public chat stream
|
|
11
|
+
* emits a structured chunk, etc.
|
|
12
|
+
*
|
|
13
|
+
* Carries the approval id + expiry so the surface can echo them back to
|
|
14
|
+
* the consumer without an extra DB read.
|
|
15
|
+
*/
|
|
16
|
+
class ToolApprovalRequired extends Error {
|
|
17
|
+
constructor(args) {
|
|
18
|
+
super(`Tool "${args.toolName}" requires approval (id ${args.approvalId}); ` +
|
|
19
|
+
`decision needed before ${args.expiresAt}`);
|
|
20
|
+
this.name = 'ToolApprovalRequired';
|
|
21
|
+
this.approvalId = args.approvalId;
|
|
22
|
+
this.toolName = args.toolName;
|
|
23
|
+
this.expiresAt = args.expiresAt;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.ToolApprovalRequired = ToolApprovalRequired;
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by the runner when the gate returns `blocked`. Different class
|
|
29
|
+
* than `ToolApprovalRequired` so callers can branch — `awaiting_approval`
|
|
30
|
+
* is a pause, `blocked` is a hard refusal.
|
|
31
|
+
*/
|
|
32
|
+
class ToolBlockedError extends Error {
|
|
33
|
+
constructor(toolName, reason) {
|
|
34
|
+
super(reason ?? `Tool "${toolName}" is blocked for this agent`);
|
|
35
|
+
this.name = 'ToolBlockedError';
|
|
36
|
+
this.toolName = toolName;
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.ToolBlockedError = ToolBlockedError;
|
|
41
|
+
/**
|
|
42
|
+
* Cheap helpers for catch-blocks that want to know whether a thrown
|
|
43
|
+
* error escaped from the gate path. Using these avoids `instanceof`
|
|
44
|
+
* realm pitfalls when the lib's compiled code is imported by both the
|
|
45
|
+
* platform's source tree and a vendored copy in tests.
|
|
46
|
+
*/
|
|
47
|
+
function isToolApprovalRequired(err) {
|
|
48
|
+
return (err instanceof ToolApprovalRequired ||
|
|
49
|
+
(typeof err === 'object' &&
|
|
50
|
+
err !== null &&
|
|
51
|
+
err.name === 'ToolApprovalRequired'));
|
|
52
|
+
}
|
|
53
|
+
function isToolBlockedError(err) {
|
|
54
|
+
return (err instanceof ToolBlockedError ||
|
|
55
|
+
(typeof err === 'object' &&
|
|
56
|
+
err !== null &&
|
|
57
|
+
err.name === 'ToolBlockedError'));
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.10",
|
|
4
4
|
"description": "Framework-free AI runtime SDK. Owns: agent loop (Anthropic), conversations, tools, streaming, agent-job queue, SdkHooks. Identity, billing, infra (email/uploads/secrets) live in the host's modules — not here.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|