@stigmer/runner 3.12.2 → 3.12.4
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/.build-fingerprint +1 -1
- package/dist/activities/call-http.js +12 -0
- package/dist/activities/call-http.js.map +1 -1
- package/dist/activities/call-llm.d.ts +18 -0
- package/dist/activities/call-llm.js +56 -2
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/execute-cursor/agent-session-cache.d.ts +72 -0
- package/dist/activities/execute-cursor/agent-session-cache.js +186 -0
- package/dist/activities/execute-cursor/agent-session-cache.js.map +1 -0
- package/dist/activities/execute-cursor/index.js +61 -28
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +5 -15
- package/dist/activities/execute-cursor/service-tier.js +5 -21
- package/dist/activities/execute-cursor/service-tier.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +15 -0
- package/dist/activities/execute-cursor/skill-resolver.js +45 -7
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +9 -1
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +13 -2
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +14 -0
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +8 -5
- package/dist/activities/execute-deep-agent/shell-env.js +10 -7
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +5 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.js +9 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +9 -1
- package/dist/client/stigmer-client.js +10 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/config.js +10 -5
- package/dist/config.js.map +1 -1
- package/dist/encryption/config.js +7 -2
- package/dist/encryption/config.js.map +1 -1
- package/dist/main.js +12 -6
- package/dist/main.js.map +1 -1
- package/dist/middleware/index.d.ts +6 -5
- package/dist/middleware/index.js +8 -5
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/tool-intent.d.ts +57 -0
- package/dist/middleware/tool-intent.js +152 -0
- package/dist/middleware/tool-intent.js.map +1 -0
- package/dist/payload-codecs.js +2 -1
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +20 -7
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +19 -6
- package/dist/runner.js.map +1 -1
- package/dist/shared/fingerprint-secret.d.ts +3 -2
- package/dist/shared/fingerprint-secret.js +5 -3
- package/dist/shared/fingerprint-secret.js.map +1 -1
- package/dist/shared/llm-backend.js +8 -1
- package/dist/shared/llm-backend.js.map +1 -1
- package/dist/shared/model-client.d.ts +15 -0
- package/dist/shared/model-client.js +57 -13
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/registry-endpoint.d.ts +5 -0
- package/dist/shared/registry-endpoint.js +7 -1
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +26 -1
- package/dist/shared/runner-credential-keys.js +34 -1
- package/dist/shared/runner-credential-keys.js.map +1 -1
- package/dist/shared/runner-credential-store.d.ts +77 -0
- package/dist/shared/runner-credential-store.js +111 -0
- package/dist/shared/runner-credential-store.js.map +1 -0
- package/dist/shared/service-tier.d.ts +55 -0
- package/dist/shared/service-tier.js +67 -0
- package/dist/shared/service-tier.js.map +1 -0
- package/dist/shared/skill-writer.js +2 -2
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/zip-extract.d.ts +10 -3
- package/dist/shared/zip-extract.js +10 -3
- package/dist/shared/zip-extract.js.map +1 -1
- package/dist/workflow-engine/tasks/call-function.d.ts +14 -0
- package/dist/workflow-engine/tasks/call-function.js +49 -5
- package/dist/workflow-engine/tasks/call-function.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +6 -0
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/engine-core.js +36 -8
- package/dist/workflows/engine-core.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/__tests__/call-http.test.ts +36 -0
- package/src/activities/__tests__/call-llm.test.ts +77 -0
- package/src/activities/call-http.ts +17 -0
- package/src/activities/call-llm.ts +78 -2
- package/src/activities/execute-cursor/__tests__/agent-session-cache.test.ts +220 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +104 -1
- package/src/activities/execute-cursor/agent-session-cache.ts +229 -0
- package/src/activities/execute-cursor/index.ts +66 -20
- package/src/activities/execute-cursor/service-tier.ts +5 -29
- package/src/activities/execute-cursor/skill-resolver.ts +52 -8
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +13 -2
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +21 -20
- package/src/activities/execute-deep-agent/setup.ts +15 -0
- package/src/activities/execute-deep-agent/shell-env.ts +10 -7
- package/src/activities/execute-deep-agent/subagent-wiring.ts +10 -1
- package/src/client/stigmer-client.ts +12 -1
- package/src/config.ts +10 -5
- package/src/encryption/config.ts +8 -2
- package/src/main.ts +16 -6
- package/src/middleware/__tests__/tool-intent.test.ts +266 -0
- package/src/middleware/index.ts +9 -5
- package/src/middleware/tool-intent.ts +174 -0
- package/src/payload-codecs.ts +2 -1
- package/src/runner-manager.ts +29 -6
- package/src/runner.ts +25 -6
- package/src/shared/__tests__/model-client.test.ts +99 -0
- package/src/shared/__tests__/runner-credential-store.test.ts +155 -0
- package/src/shared/__tests__/zip-extract.test.ts +46 -11
- package/src/shared/fingerprint-secret.ts +5 -3
- package/src/shared/llm-backend.ts +7 -1
- package/src/shared/model-client.ts +76 -13
- package/src/shared/registry-endpoint.ts +9 -1
- package/src/shared/runner-credential-keys.ts +36 -1
- package/src/shared/runner-credential-store.ts +115 -0
- package/src/shared/service-tier.ts +78 -0
- package/src/shared/skill-writer.ts +2 -2
- package/src/shared/zip-extract.ts +14 -7
- package/src/workflow-engine/__tests__/golden-execution.test.ts +20 -1
- package/src/workflow-engine/__tests__/tasks/call-function.test.ts +105 -0
- package/src/workflow-engine/tasks/call-function.ts +74 -13
- package/src/workflow-engine/types.ts +6 -0
- package/src/workflows/engine-core.ts +39 -8
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shell environment for the native harness `execute` tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Since #508's boot capture, runner secrets never LIVE in `process.env`, so
|
|
5
|
+
* the denylist below is defense-in-depth: it keeps this surface safe even if
|
|
6
|
+
* something re-plants a secret in the environment after boot (a test, an
|
|
7
|
+
* embedder, a future regression). Still built per execution — the snapshot
|
|
8
|
+
* must reflect the env as it is now, not at process start.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import {
|
|
11
|
+
import { RUNNER_SECRET_ENV_KEYS } from "../../shared/runner-credential-keys.js";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Runner-internal keys that must never reach agent shell commands: every
|
|
13
|
-
* credential the runner holds for its own outbound calls (issue #385)
|
|
14
|
-
*
|
|
15
|
+
* credential the runner holds for its own outbound calls (issue #385) plus
|
|
16
|
+
* the runner's encryption keys (issue #508). The names — and the rules for
|
|
17
|
+
* adding one — live in runner-credential-keys.ts.
|
|
15
18
|
*/
|
|
16
|
-
export const SHELL_ENV_DENYLIST: readonly string[] =
|
|
19
|
+
export const SHELL_ENV_DENYLIST: readonly string[] = RUNNER_SECRET_ENV_KEYS;
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Build the environment map passed to deepagents' LocalShellBackend.
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* same `permissions` option it bakes into the graph, keeping the rules
|
|
10
10
|
* and their normalization shim coupled.
|
|
11
11
|
* - Fresh loop detection (independent cycle tracking)
|
|
12
|
+
* - Tool intent (issue #276) — the shell tool's bind-time schema gains the
|
|
13
|
+
* optional model-authored `description`, so sub-agent shell rows carry
|
|
14
|
+
* intent titles exactly like the parent's
|
|
12
15
|
* - Fresh tool truncation (same limits as parent)
|
|
13
16
|
* - Periodic execution budget (interval=30, max=4 advisories)
|
|
14
17
|
* - Approval gate (so a mutating tool *inside* a sub-agent is gated, not
|
|
@@ -43,6 +46,7 @@ import { createPathNormalizationMiddleware } from "../../middleware/path-normali
|
|
|
43
46
|
import { createLoopDetectionMiddleware } from "../../middleware/loop-detection.js";
|
|
44
47
|
import { createToolTruncationMiddleware } from "../../middleware/tool-truncation.js";
|
|
45
48
|
import { createExecutionBudgetMiddleware } from "../../middleware/execution-budget.js";
|
|
49
|
+
import { createToolIntentMiddleware } from "../../middleware/tool-intent.js";
|
|
46
50
|
import {
|
|
47
51
|
createApprovalGateMiddleware,
|
|
48
52
|
type ApprovalGateConfig,
|
|
@@ -83,7 +87,8 @@ export interface SubAgentMiddlewareOptions {
|
|
|
83
87
|
*
|
|
84
88
|
* Returns an ordered array mirroring the parent composition:
|
|
85
89
|
* [path normalization] → loop detection → execution budget (periodic) →
|
|
86
|
-
* tool truncation → [approval gate] → cost cap view →
|
|
90
|
+
* tool intent → tool truncation → [approval gate] → cost cap view →
|
|
91
|
+
* error hints.
|
|
87
92
|
* Normalization is outermost so everything downstream observes canonical
|
|
88
93
|
* workspace-absolute paths (matching the parent). The gate sits before the
|
|
89
94
|
* cost-cap view so an approval pause happens before budget accounting, and
|
|
@@ -107,6 +112,10 @@ export function buildSubAgentMiddleware(
|
|
|
107
112
|
maxWarnings: SUB_AGENT_MAX_ADVISORIES,
|
|
108
113
|
}));
|
|
109
114
|
|
|
115
|
+
// Sub-agent shell rows render in the same thread as the parent's and must
|
|
116
|
+
// carry the same model-authored intent titles (issue #276).
|
|
117
|
+
stack.push(createToolIntentMiddleware());
|
|
118
|
+
|
|
110
119
|
stack.push(createToolTruncationMiddleware(options.toolTruncation));
|
|
111
120
|
|
|
112
121
|
if (options.approvalGate) {
|
|
@@ -35,7 +35,7 @@ import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinst
|
|
|
35
35
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
36
36
|
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
37
37
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
38
|
-
import type { GetArtifactResponse } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
|
|
38
|
+
import type { GetArtifactResponse, SkillArtifactDownloadUrl } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
|
|
39
39
|
import { create } from "@bufbuild/protobuf";
|
|
40
40
|
import { ConnectInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
41
41
|
import { ExecutionValueSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
|
|
@@ -583,6 +583,17 @@ export class StigmerClient {
|
|
|
583
583
|
return this.skillQuery.getArtifact({ artifactStorageKey });
|
|
584
584
|
}
|
|
585
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Mint an HTTP download URL for a skill artifact (#675). Preferred over
|
|
588
|
+
* getSkillArtifact for the actual bytes: the unary response is capped by
|
|
589
|
+
* the server's 10MB gRPC message limit, while skills may be 100MB.
|
|
590
|
+
* Throws ConnectError with Code.Unimplemented against servers that
|
|
591
|
+
* predate the transfer lane — callers fall back to getSkillArtifact.
|
|
592
|
+
*/
|
|
593
|
+
async getSkillArtifactDownloadUrl(artifactStorageKey: string): Promise<SkillArtifactDownloadUrl> {
|
|
594
|
+
return this.skillQuery.getArtifactDownloadUrl({ artifactStorageKey });
|
|
595
|
+
}
|
|
596
|
+
|
|
586
597
|
async createArtifact(input: CreateArtifactInput): Promise<Artifact> {
|
|
587
598
|
return this.artifactCommand.create(input);
|
|
588
599
|
}
|
package/src/config.ts
CHANGED
|
@@ -53,6 +53,7 @@ export const DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS = 120_000;
|
|
|
53
53
|
// three construction sites — mirrors DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS).
|
|
54
54
|
export { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
|
|
55
55
|
import { DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./shared/workspace/workspace-lock.js";
|
|
56
|
+
import { getRunnerSecret } from "./shared/runner-credential-store.js";
|
|
56
57
|
|
|
57
58
|
export interface Config {
|
|
58
59
|
readonly taskQueue: string;
|
|
@@ -158,9 +159,13 @@ export function loadConfig(): Config {
|
|
|
158
159
|
: requireEnv("STIGMER_BACKEND_ENDPOINT"),
|
|
159
160
|
);
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// Secrets resolve through the credential store, not process.env — the
|
|
163
|
+
// boot capture has already moved them out of the environment (#508).
|
|
164
|
+
const stigmerTokenValue = getRunnerSecret("STIGMER_TOKEN");
|
|
165
|
+
if ((mode === "cloud" || proxyActive) && !stigmerTokenValue) {
|
|
166
|
+
throw new Error("Required environment variable STIGMER_TOKEN is not set");
|
|
167
|
+
}
|
|
168
|
+
const stigmerToken = stigmerTokenValue ?? null;
|
|
164
169
|
|
|
165
170
|
const mcpBridgeEndpoint = process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null;
|
|
166
171
|
|
|
@@ -170,8 +175,8 @@ export function loadConfig(): Config {
|
|
|
170
175
|
// the SDK's authorization header (Cursor access token) passes through to
|
|
171
176
|
// api2.cursor.sh unchanged.
|
|
172
177
|
const cursorApiKey = proxyActive
|
|
173
|
-
? (
|
|
174
|
-
: (
|
|
178
|
+
? (getRunnerSecret("CURSOR_API_KEY") ?? stigmerToken ?? "proxy-managed")
|
|
179
|
+
: (getRunnerSecret("CURSOR_API_KEY") ?? "");
|
|
175
180
|
|
|
176
181
|
const workspaceRootDir = resolveWorkspaceRootDir();
|
|
177
182
|
|
package/src/encryption/config.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* on the next runner boot — there is no live re-key.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { getRunnerSecret } from "../shared/runner-credential-store.js";
|
|
29
|
+
|
|
28
30
|
export interface EncryptionKey {
|
|
29
31
|
readonly keyId: string;
|
|
30
32
|
/** 32-byte AES-256 key. */
|
|
@@ -75,14 +77,18 @@ const AES_256_KEY_BYTES = 32;
|
|
|
75
77
|
export function loadPayloadEncryptionConfig(
|
|
76
78
|
bootstrap?: BootstrapKeyMaterial,
|
|
77
79
|
): PayloadEncryptionConfig | undefined {
|
|
78
|
-
|
|
80
|
+
// Key VALUES resolve through the credential store (the #508 boot capture
|
|
81
|
+
// moves them out of process.env — agent shells must not read them); the
|
|
82
|
+
// *_KEY_ID companions are rotation bookkeeping, not secrets, and stay
|
|
83
|
+
// plain env reads.
|
|
84
|
+
const rawKey = getRunnerSecret(KEY_ENV);
|
|
79
85
|
if (rawKey) {
|
|
80
86
|
const primary: EncryptionKey = {
|
|
81
87
|
keyId: requireKeyId(KEY_ID_ENV),
|
|
82
88
|
key: parseKey(rawKey, KEY_ENV),
|
|
83
89
|
};
|
|
84
90
|
|
|
85
|
-
const rawSecondary =
|
|
91
|
+
const rawSecondary = getRunnerSecret(SECONDARY_KEY_ENV);
|
|
86
92
|
const secondary: EncryptionKey | undefined = rawSecondary
|
|
87
93
|
? {
|
|
88
94
|
keyId: requireKeyId(SECONDARY_KEY_ID_ENV),
|
package/src/main.ts
CHANGED
|
@@ -32,6 +32,10 @@ import { createInterface } from "node:readline";
|
|
|
32
32
|
import { preflightNodeRuntime } from "./preflight.js";
|
|
33
33
|
import { markBoot, emitRunnerBootTiming } from "./shared/cold-start-timing.js";
|
|
34
34
|
import { loadConfig } from "./config.js";
|
|
35
|
+
import {
|
|
36
|
+
captureRunnerSecrets,
|
|
37
|
+
getRunnerSecret,
|
|
38
|
+
} from "./shared/runner-credential-store.js";
|
|
35
39
|
import { initTracing, initMetrics } from "./otel.js";
|
|
36
40
|
import { createStigmerRunner } from "./runner.js";
|
|
37
41
|
import { createStigmerRunnerManager } from "./runner-manager.js";
|
|
@@ -214,11 +218,12 @@ async function runPoolMode(
|
|
|
214
218
|
});
|
|
215
219
|
|
|
216
220
|
// Sandbox credential self-renewal (see sandbox-token-renewal.ts). Watches
|
|
217
|
-
// the
|
|
218
|
-
// manager's internal ref: a blank member's pool_sandbox token
|
|
219
|
-
// loop, the claim's updateToken swaps in a renewable session
|
|
220
|
-
// from then on renewal applies fresh tokens back through the
|
|
221
|
-
// updateToken (which also cascades to the proxy-credential
|
|
221
|
+
// the credential store because manager.updateToken keeps it in lockstep
|
|
222
|
+
// with the manager's internal ref: a blank member's pool_sandbox token
|
|
223
|
+
// parks the loop, the claim's updateToken swaps in a renewable session
|
|
224
|
+
// token, and from then on renewal applies fresh tokens back through the
|
|
225
|
+
// same updateToken (which also cascades to the proxy-credential
|
|
226
|
+
// coordinator).
|
|
222
227
|
const { startSandboxTokenRenewal } = await import("./sandbox-token-renewal.js");
|
|
223
228
|
const { StigmerClient } = await import("./client/stigmer-client.js");
|
|
224
229
|
const renewalClient = new StigmerClient({
|
|
@@ -226,7 +231,7 @@ async function runPoolMode(
|
|
|
226
231
|
token: null,
|
|
227
232
|
});
|
|
228
233
|
const tokenRenewal = startSandboxTokenRenewal({
|
|
229
|
-
getToken: () =>
|
|
234
|
+
getToken: () => getRunnerSecret("STIGMER_TOKEN") ?? null,
|
|
230
235
|
renew: (currentToken) =>
|
|
231
236
|
renewalClient.getRunnerScopedToken({ renewal: true }, currentToken),
|
|
232
237
|
applyToken: (token) => manager.updateToken(token),
|
|
@@ -449,6 +454,11 @@ async function main(): Promise<void> {
|
|
|
449
454
|
|
|
450
455
|
checkBuildFreshness();
|
|
451
456
|
|
|
457
|
+
// Take custody of runner secrets before the config load reads them (#508).
|
|
458
|
+
// The factories capture too (they are the library boot doors); doing it
|
|
459
|
+
// here as well keeps main.ts's own late readers store-only from the start.
|
|
460
|
+
captureRunnerSecrets();
|
|
461
|
+
|
|
452
462
|
const config = loadConfig();
|
|
453
463
|
markBoot("config_loaded");
|
|
454
464
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-intent middleware (issue #276): the shell tool's bind-time schema
|
|
3
|
+
* gains an optional model-authored `description`, execution never sees it,
|
|
4
|
+
* and the argument survives verbatim into the message history.
|
|
5
|
+
*
|
|
6
|
+
* The integration block drives a REAL deepagents graph, because the two
|
|
7
|
+
* claims that matter most are framework claims: (1) `wrapModelCall` receives
|
|
8
|
+
* the library's built-in `execute` tool and the clone reaches `bindTools`,
|
|
9
|
+
* and (2) the original tool's strip-parsing drops the extra argument before
|
|
10
|
+
* the backend's `execute(command)` runs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { mkdtemp, rm, readFile } from "node:fs/promises";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join, resolve, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { tool } from "@langchain/core/tools";
|
|
21
|
+
import { convertToOpenAITool } from "@langchain/core/utils/function_calling";
|
|
22
|
+
import { AIMessage, HumanMessage } from "@langchain/core/messages";
|
|
23
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
24
|
+
import { createDeepAgent } from "deepagents";
|
|
25
|
+
import {
|
|
26
|
+
createToolIntentMiddleware,
|
|
27
|
+
INTENT_ARG,
|
|
28
|
+
INTENT_ARG_PROMPT,
|
|
29
|
+
} from "../tool-intent.js";
|
|
30
|
+
import { buildSubAgentMiddleware } from "../../activities/execute-deep-agent/subagent-wiring.js";
|
|
31
|
+
import { createCasCaptureBackend } from "../../activities/execute-deep-agent/cas-capture-backend.js";
|
|
32
|
+
import { CasCaptureObserver } from "../../activities/execute-deep-agent/cas-capture-observer.js";
|
|
33
|
+
import {
|
|
34
|
+
ScriptedModel,
|
|
35
|
+
type ScriptSelector,
|
|
36
|
+
} from "../../activities/execute-deep-agent/__test-utils__/scripted-model.js";
|
|
37
|
+
|
|
38
|
+
/** A stand-in for deepagents' `execute` tool: same name, same schema shape. */
|
|
39
|
+
function makeShellTool(executed: string[]) {
|
|
40
|
+
return tool(
|
|
41
|
+
async ({ command }: { command: string }) => {
|
|
42
|
+
executed.push(command);
|
|
43
|
+
return "ok";
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "execute",
|
|
47
|
+
description: "Run a shell command",
|
|
48
|
+
schema: z.object({ command: z.string().describe("The shell command to execute") }),
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeReadTool() {
|
|
54
|
+
return tool(async () => "contents", {
|
|
55
|
+
name: "read_file",
|
|
56
|
+
description: "Read a file",
|
|
57
|
+
schema: z.object({ file_path: z.string() }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type BoundSchema = {
|
|
62
|
+
type?: string;
|
|
63
|
+
properties?: Record<string, { type?: string; description?: string }>;
|
|
64
|
+
required?: string[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function schemaOf(t: unknown): BoundSchema {
|
|
68
|
+
return (t as { schema: BoundSchema }).schema;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runWrap(
|
|
72
|
+
mw: ReturnType<typeof createToolIntentMiddleware>,
|
|
73
|
+
tools: unknown[],
|
|
74
|
+
): Promise<unknown[]> {
|
|
75
|
+
let seen: unknown[] = [];
|
|
76
|
+
const handler = vi.fn(async (req: { tools?: unknown[] }) => {
|
|
77
|
+
seen = req.tools ?? [];
|
|
78
|
+
return new AIMessage({ content: "" });
|
|
79
|
+
});
|
|
80
|
+
await mw.wrapModelCall!(
|
|
81
|
+
{ model: {}, messages: [], tools, state: {}, runtime: {} } as never,
|
|
82
|
+
handler as never,
|
|
83
|
+
);
|
|
84
|
+
return seen;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("ToolIntentMiddleware (unit)", () => {
|
|
88
|
+
it("passes a tool-less request through untouched", async () => {
|
|
89
|
+
const mw = createToolIntentMiddleware();
|
|
90
|
+
const request = { model: {}, messages: [], state: {}, runtime: {} } as never;
|
|
91
|
+
const handler = vi.fn(async () => new AIMessage({ content: "" }));
|
|
92
|
+
await mw.wrapModelCall!(request, handler as never);
|
|
93
|
+
expect(handler).toHaveBeenCalledWith(request);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("extends the shell tool's bound schema with the optional intent arg", async () => {
|
|
97
|
+
const shell = makeShellTool([]);
|
|
98
|
+
const bound = await runWrap(createToolIntentMiddleware(), [shell]);
|
|
99
|
+
|
|
100
|
+
expect(bound).toHaveLength(1);
|
|
101
|
+
expect(bound[0]).not.toBe(shell);
|
|
102
|
+
const schema = schemaOf(bound[0]);
|
|
103
|
+
expect(schema.properties?.command?.type).toBe("string");
|
|
104
|
+
expect(schema.properties?.[INTENT_ARG]).toEqual({
|
|
105
|
+
type: "string",
|
|
106
|
+
description: INTENT_ARG_PROMPT,
|
|
107
|
+
});
|
|
108
|
+
// Optional by construction: required is untouched.
|
|
109
|
+
expect(schema.required ?? []).not.toContain(INTENT_ARG);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("passes non-shell tools through by reference", async () => {
|
|
113
|
+
const read = makeReadTool();
|
|
114
|
+
const shell = makeShellTool([]);
|
|
115
|
+
const bound = await runWrap(createToolIntentMiddleware(), [read, shell]);
|
|
116
|
+
expect(bound[0]).toBe(read);
|
|
117
|
+
expect(bound[1]).not.toBe(shell);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("never shadows a real argument named like the intent arg", async () => {
|
|
121
|
+
const conflicting = tool(async () => "ok", {
|
|
122
|
+
name: "shell",
|
|
123
|
+
description: "A shell tool that already has a description arg",
|
|
124
|
+
schema: z.object({ command: z.string(), [INTENT_ARG]: z.string() }),
|
|
125
|
+
});
|
|
126
|
+
const bound = await runWrap(createToolIntentMiddleware(), [conflicting]);
|
|
127
|
+
expect(bound[0]).toBe(conflicting);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("passes through tools whose schema is not an object schema", async () => {
|
|
131
|
+
const odd = { name: "bash", description: "odd", schema: 42 };
|
|
132
|
+
const bound = await runWrap(createToolIntentMiddleware(), [odd]);
|
|
133
|
+
expect(bound[0]).toBe(odd);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("reuses one clone across model calls (referential stability)", async () => {
|
|
137
|
+
const mw = createToolIntentMiddleware();
|
|
138
|
+
const shell = makeShellTool([]);
|
|
139
|
+
const first = await runWrap(mw, [shell]);
|
|
140
|
+
const second = await runWrap(mw, [shell]);
|
|
141
|
+
expect(first[0]).toBe(second[0]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("emits a non-executable declaration that provider converters accept", async () => {
|
|
145
|
+
const shell = makeShellTool([]);
|
|
146
|
+
const bound = await runWrap(createToolIntentMiddleware(), [shell]);
|
|
147
|
+
const declaration = bound[0] as Record<string, unknown>;
|
|
148
|
+
|
|
149
|
+
// Deliberately NOT an executable tool — the agent's validation forbids
|
|
150
|
+
// swapping same-name executable instances, and execution belongs to the
|
|
151
|
+
// registered original. StructuredToolParams is the sanctioned shape.
|
|
152
|
+
expect(declaration.invoke).toBeUndefined();
|
|
153
|
+
|
|
154
|
+
// The shape every provider's bindTools converts like a structured tool.
|
|
155
|
+
const openAiTool = convertToOpenAITool(declaration as never) as {
|
|
156
|
+
function: { name: string; parameters: { properties: Record<string, unknown> } };
|
|
157
|
+
};
|
|
158
|
+
expect(openAiTool.function.name).toBe("execute");
|
|
159
|
+
expect(openAiTool.function.parameters.properties[INTENT_ARG]).toEqual({
|
|
160
|
+
type: "string",
|
|
161
|
+
description: INTENT_ARG_PROMPT,
|
|
162
|
+
});
|
|
163
|
+
expect(openAiTool.function.parameters.properties.command).toBeDefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("is idempotent: an already-extended declaration passes through", async () => {
|
|
167
|
+
const mw = createToolIntentMiddleware();
|
|
168
|
+
const shell = makeShellTool([]);
|
|
169
|
+
const [firstPass] = await runWrap(mw, [shell]);
|
|
170
|
+
// A second middleware instance (e.g. a sub-agent stack composed over the
|
|
171
|
+
// same request) must not re-wrap the extended declaration.
|
|
172
|
+
const [secondPass] = await runWrap(createToolIntentMiddleware(), [firstPass]);
|
|
173
|
+
expect(secondPass).toBe(firstPass);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("ToolIntentMiddleware (real deepagents graph)", () => {
|
|
178
|
+
let root: string;
|
|
179
|
+
|
|
180
|
+
beforeEach(async () => {
|
|
181
|
+
root = await mkdtemp(join(tmpdir(), "tool-intent-"));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterEach(async () => {
|
|
185
|
+
await rm(root, { recursive: true, force: true });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("binds the extended execute schema to the model and strips the arg at execution", async () => {
|
|
189
|
+
const marker = join(root, "intent-marker.txt");
|
|
190
|
+
const script: ScriptSelector = () => ({
|
|
191
|
+
toolCalls: [{
|
|
192
|
+
name: "execute",
|
|
193
|
+
args: {
|
|
194
|
+
command: "echo ran > intent-marker.txt",
|
|
195
|
+
[INTENT_ARG]: "Write the marker file",
|
|
196
|
+
},
|
|
197
|
+
id: "exec_intent_1",
|
|
198
|
+
}],
|
|
199
|
+
done: "done",
|
|
200
|
+
});
|
|
201
|
+
const model = new ScriptedModel(script);
|
|
202
|
+
|
|
203
|
+
const observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => false });
|
|
204
|
+
const backend = await createCasCaptureBackend({ rootDir: root, observer, shellEnv: {} });
|
|
205
|
+
|
|
206
|
+
const checkpointer = new MemorySaver();
|
|
207
|
+
const agent = await createDeepAgent({
|
|
208
|
+
model,
|
|
209
|
+
checkpointer: checkpointer as never,
|
|
210
|
+
backend,
|
|
211
|
+
middleware: [createToolIntentMiddleware()],
|
|
212
|
+
} as Parameters<typeof createDeepAgent>[0]);
|
|
213
|
+
|
|
214
|
+
const config = { configurable: { thread_id: "intent-thread" }, recursionLimit: 50 };
|
|
215
|
+
await agent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
|
|
216
|
+
|
|
217
|
+
// (1) The model saw the library's execute tool WITH the intent arg.
|
|
218
|
+
const boundExecute = model.boundTools.find(
|
|
219
|
+
(t) => (t as { name?: string }).name === "execute",
|
|
220
|
+
);
|
|
221
|
+
expect(boundExecute).toBeDefined();
|
|
222
|
+
const schema = schemaOf(boundExecute);
|
|
223
|
+
expect(schema.properties?.[INTENT_ARG]?.description).toBe(INTENT_ARG_PROMPT);
|
|
224
|
+
expect(schema.properties?.command).toBeDefined();
|
|
225
|
+
expect(schema.required ?? []).not.toContain(INTENT_ARG);
|
|
226
|
+
|
|
227
|
+
// (2) Execution ran the ORIGINAL tool: strip semantics dropped the intent
|
|
228
|
+
// arg and the command executed normally.
|
|
229
|
+
expect(await readFile(marker, "utf8")).toBe("ran\n");
|
|
230
|
+
|
|
231
|
+
// (3) The intent arg survived verbatim in the message history — the
|
|
232
|
+
// exact bytes the status builder persists onto ToolCall.args.
|
|
233
|
+
const state = (await agent.getState(config)) as unknown as {
|
|
234
|
+
values: { messages: Array<{ tool_calls?: Array<{ name: string; args: Record<string, unknown> }> }> };
|
|
235
|
+
};
|
|
236
|
+
const messages = state.values.messages;
|
|
237
|
+
const toolCall = messages
|
|
238
|
+
.flatMap((m) => m.tool_calls ?? [])
|
|
239
|
+
.find((tc) => tc.name === "execute");
|
|
240
|
+
expect(toolCall).toBeDefined();
|
|
241
|
+
expect(toolCall!.args[INTENT_ARG]).toBe("Write the marker file");
|
|
242
|
+
expect(toolCall!.args.command).toBe("echo ran > intent-marker.txt");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("sub-agent stack wiring", () => {
|
|
247
|
+
it("includes the tool-intent middleware in every sub-agent stack", () => {
|
|
248
|
+
const stack = buildSubAgentMiddleware({});
|
|
249
|
+
expect(stack.map((m) => m.name)).toContain("StigmerToolIntentMiddleware");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe("wire-contract fixture", () => {
|
|
254
|
+
it("INTENT_ARG matches the cross-surface fixture key the SDK reads", () => {
|
|
255
|
+
// The reader side (sdk/react intent-title tests) asserts against the
|
|
256
|
+
// same file, so the writer and readers cannot drift apart silently.
|
|
257
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
258
|
+
const fixture = JSON.parse(
|
|
259
|
+
readFileSync(
|
|
260
|
+
resolve(here, "../../../../../../test/fixtures/tool-view/intent-title.json"),
|
|
261
|
+
"utf8",
|
|
262
|
+
),
|
|
263
|
+
) as { argField: string };
|
|
264
|
+
expect(INTENT_ARG).toBe(fixture.argField);
|
|
265
|
+
});
|
|
266
|
+
});
|
package/src/middleware/index.ts
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* workspace-absolute paths)
|
|
10
10
|
* 1. Loop detection (always)
|
|
11
11
|
* 2. Execution budget (always)
|
|
12
|
-
* 3. Tool
|
|
13
|
-
* 4.
|
|
14
|
-
* 5.
|
|
15
|
-
* 6.
|
|
16
|
-
* 7.
|
|
12
|
+
* 3. Tool intent (always — bind-time shell schema extension, issue #276)
|
|
13
|
+
* 4. Tool truncation (always)
|
|
14
|
+
* 5. Graceful stop (always, inert until activated)
|
|
15
|
+
* 6. Cost cap (conditional: only when maxCostUsd > 0)
|
|
16
|
+
* 7. Error hints (always)
|
|
17
|
+
* 8. OTel spans (always, no-op when OTel not configured)
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import type { StigmerMiddleware, MiddlewareStackConfig } from "./types.js";
|
|
@@ -21,6 +22,7 @@ import type { GracefulStopMiddleware } from "./graceful-stop.js";
|
|
|
21
22
|
import { createPathNormalizationMiddleware } from "./path-normalization.js";
|
|
22
23
|
import { createLoopDetectionMiddleware } from "./loop-detection.js";
|
|
23
24
|
import { createExecutionBudgetMiddleware } from "./execution-budget.js";
|
|
25
|
+
import { createToolIntentMiddleware } from "./tool-intent.js";
|
|
24
26
|
import { createToolTruncationMiddleware } from "./tool-truncation.js";
|
|
25
27
|
import { createGracefulStopMiddleware } from "./graceful-stop.js";
|
|
26
28
|
import { createApprovalGateMiddleware } from "./approval-gate.js";
|
|
@@ -51,6 +53,8 @@ export function buildMiddlewareStack(
|
|
|
51
53
|
|
|
52
54
|
stack.push(createExecutionBudgetMiddleware(config.executionBudget));
|
|
53
55
|
|
|
56
|
+
stack.push(createToolIntentMiddleware());
|
|
57
|
+
|
|
54
58
|
stack.push(createToolTruncationMiddleware(config.toolTruncation));
|
|
55
59
|
|
|
56
60
|
const gracefulStop = createGracefulStopMiddleware();
|