@cursor/july 0.1.83 → 0.1.84
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/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.6THHGRH-.js → app.kqdC1N7p.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BqhvUA76.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BpHB9ulP.js → VPLocalSearchBox.DXDE2WgG.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DBJvJznv.js → theme.DFYqbyta.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/convert-automation.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/opentelemetry.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/artifacts.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/templates/agentic-owners.html +2 -2
- package/dist/docs/templates/demo.html +2 -2
- package/dist/docs/templates/pr-autofixer.html +2 -2
- package/dist/docs/templates/security-reviewer.html +2 -2
- package/dist/docs/templates/triage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +16 -1
- package/dist/internal/event-mapper.d.ts +7 -1
- package/dist/internal/event-mapper.d.ts.map +1 -1
- package/dist/internal/event-mapper.js +8 -8
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +1 -1
- package/dist/internal/mcp-endpoint.js +9 -0
- package/dist/internal/prompt-context.d.ts +4 -1
- package/dist/internal/prompt-context.d.ts.map +1 -1
- package/dist/internal/prompt-context.js +20 -2
- package/dist/internal/review-comments.js +15 -7
- package/dist/internal/session-engine.d.ts +28 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +99 -42
- package/dist/internal/tool-policy.d.ts +50 -0
- package/dist/internal/tool-policy.d.ts.map +1 -0
- package/dist/internal/tool-policy.js +51 -0
- package/dist/tools.d.ts +16 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/internal/discovery.ts +20 -0
- package/src/internal/event-mapper.ts +10 -1
- package/src/internal/http-channel.ts +1 -0
- package/src/internal/mcp-endpoint.ts +9 -0
- package/src/internal/prompt-context.ts +25 -2
- package/src/internal/review-comments.ts +17 -7
- package/src/internal/session-engine.ts +120 -31
- package/src/internal/tool-policy.ts +92 -0
- package/src/tools.ts +19 -0
- package/src/types.ts +73 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.D9uScPWb.js +0 -1
|
@@ -134,6 +134,7 @@ import { SessionStore } from "./session-store.js";
|
|
|
134
134
|
import { syncSkillsToStore } from "./skills-sync.js";
|
|
135
135
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
136
136
|
import type { FrameworkSessionStore } from "./storage-roles.js";
|
|
137
|
+
import { decideToolCall } from "./tool-policy.js";
|
|
137
138
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
138
139
|
import {
|
|
139
140
|
type EstimateTurnCostFn,
|
|
@@ -333,6 +334,15 @@ export class SessionEngine {
|
|
|
333
334
|
private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
|
|
334
335
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
335
336
|
private readonly activeToolContexts = new Map<string, ActiveToolContext>();
|
|
337
|
+
/**
|
|
338
|
+
* Tool call ids the host answered instead of running, per session.
|
|
339
|
+
*
|
|
340
|
+
* The mapper emits the result event from the harness's view of a finished
|
|
341
|
+
* call, which cannot tell an answered call from a real one — so the decision
|
|
342
|
+
* point records the id here and the mapper stamps the event from it.
|
|
343
|
+
* Registered only while a turn can invoke a tool.
|
|
344
|
+
*/
|
|
345
|
+
private readonly stubbedCallIds = new Map<string, Set<string>>();
|
|
336
346
|
/**
|
|
337
347
|
* Delivered-nudge count per session at the last tool boundary. The carrier
|
|
338
348
|
* drains out-of-process, so a growing count is how the engine learns a
|
|
@@ -882,6 +892,7 @@ export class SessionEngine {
|
|
|
882
892
|
channelId,
|
|
883
893
|
mode: sendOptions.mode ?? "chat",
|
|
884
894
|
purpose,
|
|
895
|
+
...(sendOptions.dryRun === true ? { dryRun: true } : {}),
|
|
885
896
|
continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
|
|
886
897
|
auth,
|
|
887
898
|
title: sendOptions.title,
|
|
@@ -1596,7 +1607,10 @@ export class SessionEngine {
|
|
|
1596
1607
|
);
|
|
1597
1608
|
}
|
|
1598
1609
|
};
|
|
1599
|
-
|
|
1610
|
+
// Read by closure, so the mapper still sees it while draining after the
|
|
1611
|
+
// registry entry below is gone.
|
|
1612
|
+
const stubbed = new Set<string>();
|
|
1613
|
+
const mapper = new TurnEventMapper(emit, (callId) => stubbed.has(callId));
|
|
1600
1614
|
|
|
1601
1615
|
// Take a turn slot before any work happens. When every slot is busy the
|
|
1602
1616
|
// turn waits FIFO — durable `turn.queued` marks the wait — and a
|
|
@@ -1706,10 +1720,12 @@ export class SessionEngine {
|
|
|
1706
1720
|
pendingImages
|
|
1707
1721
|
);
|
|
1708
1722
|
this.activeToolContexts.set(sessionId, { record, turnId, emit });
|
|
1723
|
+
this.stubbedCallIds.set(sessionId, stubbed);
|
|
1709
1724
|
try {
|
|
1710
1725
|
outcome = await this.runner.runTurn(request);
|
|
1711
1726
|
} finally {
|
|
1712
1727
|
this.activeToolContexts.delete(sessionId);
|
|
1728
|
+
this.stubbedCallIds.delete(sessionId);
|
|
1713
1729
|
}
|
|
1714
1730
|
} catch (error) {
|
|
1715
1731
|
this.approvals.cancelSession(sessionId, "deny");
|
|
@@ -2434,6 +2450,21 @@ export class SessionEngine {
|
|
|
2434
2450
|
emit: args.emit,
|
|
2435
2451
|
};
|
|
2436
2452
|
try {
|
|
2453
|
+
// Before the approval gate: a dry run is unattended, so parking a write
|
|
2454
|
+
// for a human would hang the turn rather than answer it.
|
|
2455
|
+
const decision = decideToolCall({
|
|
2456
|
+
dryRun: args.record.dryRun === true,
|
|
2457
|
+
effect: args.tool.definition.effect,
|
|
2458
|
+
dryRunResult: args.tool.definition.dryRunResult,
|
|
2459
|
+
input: validation.value,
|
|
2460
|
+
});
|
|
2461
|
+
if (decision.kind === "answer") {
|
|
2462
|
+
this.stubbedCallIds.get(args.record.sessionId)?.add(toolCallId);
|
|
2463
|
+
this.logger(
|
|
2464
|
+
`[session] tool ${args.tool.name} answered by host (${toolCallId})`
|
|
2465
|
+
);
|
|
2466
|
+
return normalizeToolResult(decision.result);
|
|
2467
|
+
}
|
|
2437
2468
|
const needsGate = await evaluateNeedsApproval(
|
|
2438
2469
|
args.tool.definition.needsApproval,
|
|
2439
2470
|
validation.value
|
|
@@ -2716,10 +2747,21 @@ export class SessionEngine {
|
|
|
2716
2747
|
});
|
|
2717
2748
|
}
|
|
2718
2749
|
|
|
2750
|
+
// A separate body from the model path, so it asks the same policy: a
|
|
2751
|
+
// session-bound `POST /v1/tools/:name`, `call --session`, or
|
|
2752
|
+
// `handle.callTool` performs a write just as readily.
|
|
2753
|
+
const decision = decideToolCall({
|
|
2754
|
+
dryRun: record?.dryRun === true,
|
|
2755
|
+
effect: tool.definition.effect,
|
|
2756
|
+
dryRunResult: tool.definition.dryRunResult,
|
|
2757
|
+
input: validation.value,
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2719
2760
|
const started = Date.now();
|
|
2720
2761
|
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
2721
2762
|
let result: unknown;
|
|
2722
2763
|
let isError: boolean;
|
|
2764
|
+
const stubbed = decision.kind === "answer";
|
|
2723
2765
|
const live = record;
|
|
2724
2766
|
const emit =
|
|
2725
2767
|
live === undefined
|
|
@@ -2731,35 +2773,43 @@ export class SessionEngine {
|
|
|
2731
2773
|
}).catch(() => {});
|
|
2732
2774
|
};
|
|
2733
2775
|
try {
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
})
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2776
|
+
if (decision.kind === "answer") {
|
|
2777
|
+
this.logger(
|
|
2778
|
+
`[session] tool ${toolName} answered by host (direct ${callId})`
|
|
2779
|
+
);
|
|
2780
|
+
result = normalizeToolResult(decision.result);
|
|
2781
|
+
isError = false;
|
|
2782
|
+
} else {
|
|
2783
|
+
result = normalizeToolResult(
|
|
2784
|
+
await execute(validation.value as never, {
|
|
2785
|
+
toolCallId: callId,
|
|
2786
|
+
session,
|
|
2787
|
+
workspaceDir,
|
|
2788
|
+
stateRoot: this.stateRoot,
|
|
2789
|
+
host: this.hostForSession(record?.sessionId),
|
|
2790
|
+
// Scratch calls have no real session; their facade stays unbound.
|
|
2791
|
+
artifacts:
|
|
2792
|
+
record === undefined
|
|
2793
|
+
? this.artifactsApi()
|
|
2794
|
+
: this.artifactsApi({
|
|
2795
|
+
sessionId: record.sessionId,
|
|
2796
|
+
turnId: callId,
|
|
2797
|
+
}),
|
|
2798
|
+
send: (channelId, message, sendOptions = {}) =>
|
|
2799
|
+
this.send(channelId, message, {
|
|
2800
|
+
...sendOptions,
|
|
2801
|
+
auth:
|
|
2802
|
+
sendOptions.auth !== undefined
|
|
2803
|
+
? sendOptions.auth
|
|
2804
|
+
: session.auth,
|
|
2805
|
+
}),
|
|
2806
|
+
getSession: (channelId, sessionId) =>
|
|
2807
|
+
this.getChannelSession(channelId, sessionId),
|
|
2808
|
+
emit,
|
|
2809
|
+
})
|
|
2810
|
+
);
|
|
2811
|
+
isError = isErrorEnvelope(result);
|
|
2812
|
+
}
|
|
2763
2813
|
} catch (error) {
|
|
2764
2814
|
this.logger(
|
|
2765
2815
|
`[session] tool ${toolName} failed (direct ${callId}): ${describeError(error)}`
|
|
@@ -2789,7 +2839,13 @@ export class SessionEngine {
|
|
|
2789
2839
|
await this.appendEvent(record.sessionId, {
|
|
2790
2840
|
type: "action.result",
|
|
2791
2841
|
turnId: callId,
|
|
2792
|
-
data: {
|
|
2842
|
+
data: {
|
|
2843
|
+
callId,
|
|
2844
|
+
toolName,
|
|
2845
|
+
output: result,
|
|
2846
|
+
isError,
|
|
2847
|
+
...(stubbed ? { stubbed: true as const } : {}),
|
|
2848
|
+
},
|
|
2793
2849
|
});
|
|
2794
2850
|
}
|
|
2795
2851
|
|
|
@@ -2805,6 +2861,38 @@ export class SessionEngine {
|
|
|
2805
2861
|
};
|
|
2806
2862
|
}
|
|
2807
2863
|
|
|
2864
|
+
/**
|
|
2865
|
+
* Whether a dry run answers a bridged MCP call, for the cloud host bridge —
|
|
2866
|
+
* the one tool path that does not reach {@link executeServerTool}, because
|
|
2867
|
+
* it forwards to `host.mcp` instead of invoking a tool body.
|
|
2868
|
+
*
|
|
2869
|
+
* A bridged tool carries no `defineTool`, so it declares no effect, and the
|
|
2870
|
+
* policy reads an undeclared tool as a write. In a dry run that answers
|
|
2871
|
+
* bridged reads too; resolving effects from a server's own listing is the
|
|
2872
|
+
* next unit's work.
|
|
2873
|
+
*/
|
|
2874
|
+
bridgedCallAnswer(args: {
|
|
2875
|
+
sessionId: string;
|
|
2876
|
+
toolName: string;
|
|
2877
|
+
}): { answered: true; result: SDKCustomToolResult } | { answered: false } {
|
|
2878
|
+
// Read off the in-flight turn's context rather than the session store:
|
|
2879
|
+
// the bridge already refuses a call with no active turn, so the record is
|
|
2880
|
+
// in memory. That keeps this free for the sessions that are not dry runs,
|
|
2881
|
+
// which is every session today.
|
|
2882
|
+
const record = this.activeToolContexts.get(args.sessionId)?.record;
|
|
2883
|
+
const decision = decideToolCall({
|
|
2884
|
+
dryRun: record?.dryRun === true,
|
|
2885
|
+
effect: undefined,
|
|
2886
|
+
});
|
|
2887
|
+
if (decision.kind === "run") {
|
|
2888
|
+
return { answered: false };
|
|
2889
|
+
}
|
|
2890
|
+
this.logger(
|
|
2891
|
+
`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`
|
|
2892
|
+
);
|
|
2893
|
+
return { answered: true, result: normalizeToolResult(decision.result) };
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2808
2896
|
// ==========================================================================
|
|
2809
2897
|
// Events: append, dispatch to channel handlers + hooks
|
|
2810
2898
|
// ==========================================================================
|
|
@@ -3445,6 +3533,7 @@ export class SessionEngine {
|
|
|
3445
3533
|
channelId: record.channelId,
|
|
3446
3534
|
mode: record.mode,
|
|
3447
3535
|
purpose: record.purpose === "eval" ? "eval" : "live",
|
|
3536
|
+
...(record.dryRun === true ? { dryRun: true } : {}),
|
|
3448
3537
|
auth: record.auth,
|
|
3449
3538
|
continuationKey: record.continuationKey,
|
|
3450
3539
|
...(record.abs === undefined ? {} : { abs: record.abs }),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one decision every tool call passes through.
|
|
3
|
+
*
|
|
4
|
+
* A tool body can be reached by more than one path — the model's own calls on
|
|
5
|
+
* a local turn, a cloud turn's calls over the MCP endpoint, a direct
|
|
6
|
+
* `callTool`, the host connection bridge. Each of those is a separate code
|
|
7
|
+
* path, and a guarantee that holds on some of them is not a guarantee. So the
|
|
8
|
+
* decision lives here, in one pure function, and every path asks it rather
|
|
9
|
+
* than re-implementing it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ToolEffect,
|
|
14
|
+
ToolEffectDeclaration,
|
|
15
|
+
ToolExecuteResult,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
|
|
18
|
+
/** What the host does with one tool call. */
|
|
19
|
+
export type ToolDecision =
|
|
20
|
+
/** Run the tool body. */
|
|
21
|
+
| { kind: "run" }
|
|
22
|
+
/** Do not run it; return this to the model instead. */
|
|
23
|
+
| { kind: "answer"; result: ToolExecuteResult };
|
|
24
|
+
|
|
25
|
+
/** What a policy decision needs to know about the call. */
|
|
26
|
+
export interface ToolCallFacts {
|
|
27
|
+
/** The session answers writes instead of running them. */
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
/** The tool's declaration, if it made one. */
|
|
30
|
+
effect: ToolEffectDeclaration<never> | undefined;
|
|
31
|
+
/** What a stubbed write should answer with, if the tool supplied one. */
|
|
32
|
+
dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
|
|
33
|
+
/**
|
|
34
|
+
* Validated input, as the tool body would receive it. Only read by a
|
|
35
|
+
* per-call `effect` predicate and by a `dryRunResult` function, so a caller
|
|
36
|
+
* with neither — a bridged MCP tool, which carries no declaration at all —
|
|
37
|
+
* can omit it.
|
|
38
|
+
*/
|
|
39
|
+
input?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What a stubbed write answers. Deliberately not self-describing: a model told
|
|
44
|
+
* mid-turn that its writes are not real starts hedging its conclusions, and
|
|
45
|
+
* the conclusion is the thing a dry run exists to read. Tell the agent once,
|
|
46
|
+
* in its opening prompt, not in a tool result.
|
|
47
|
+
*/
|
|
48
|
+
const ACKNOWLEDGED = "Operation acknowledged.";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The effect of one specific call.
|
|
52
|
+
*
|
|
53
|
+
* Undeclared resolves to `"write"`. A dry run promises that nothing outside
|
|
54
|
+
* the session changed, and it cannot promise that about a tool it knows
|
|
55
|
+
* nothing about — so the unknown case is the one that costs a stubbed read
|
|
56
|
+
* rather than the one that leaks a real write.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveEffect(
|
|
59
|
+
effect: ToolEffectDeclaration<never> | undefined,
|
|
60
|
+
input: unknown
|
|
61
|
+
): ToolEffect {
|
|
62
|
+
const declared =
|
|
63
|
+
typeof effect === "function" ? effect(input as never) : effect;
|
|
64
|
+
return declared === "read" ? "read" : "write";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
68
|
+
export function decideToolCall(facts: ToolCallFacts): ToolDecision {
|
|
69
|
+
if (!facts.dryRun) {
|
|
70
|
+
return { kind: "run" };
|
|
71
|
+
}
|
|
72
|
+
if (resolveEffect(facts.effect, facts.input) === "read") {
|
|
73
|
+
return { kind: "run" };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
kind: "answer",
|
|
77
|
+
result: dryRunAnswer(facts.dryRunResult, facts.input),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dryRunAnswer(
|
|
82
|
+
dryRunResult: ToolCallFacts["dryRunResult"],
|
|
83
|
+
input: unknown
|
|
84
|
+
): ToolExecuteResult {
|
|
85
|
+
if (dryRunResult === undefined) {
|
|
86
|
+
return ACKNOWLEDGED;
|
|
87
|
+
}
|
|
88
|
+
if (typeof dryRunResult === "function") {
|
|
89
|
+
return dryRunResult(input as never);
|
|
90
|
+
}
|
|
91
|
+
return dryRunResult;
|
|
92
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -59,6 +59,7 @@ import type {
|
|
|
59
59
|
JsonSchemaObject,
|
|
60
60
|
ToolContext,
|
|
61
61
|
ToolDefinition,
|
|
62
|
+
ToolEffectDeclaration,
|
|
62
63
|
ToolExecuteResult,
|
|
63
64
|
ToolNeedsApproval,
|
|
64
65
|
} from "./types.js";
|
|
@@ -70,6 +71,8 @@ export type {
|
|
|
70
71
|
ToolConfig,
|
|
71
72
|
ToolContext,
|
|
72
73
|
ToolDefinition,
|
|
74
|
+
ToolEffect,
|
|
75
|
+
ToolEffectDeclaration,
|
|
73
76
|
ToolExecuteResult,
|
|
74
77
|
ToolExecution,
|
|
75
78
|
ToolNeedsApproval,
|
|
@@ -91,6 +94,15 @@ type ServerToolConfig<TSchema> = {
|
|
|
91
94
|
*/
|
|
92
95
|
outputSchema?: z.ZodTypeAny | JsonSchemaObject;
|
|
93
96
|
needsApproval?: ToolNeedsApproval<ToolInput<TSchema>>;
|
|
97
|
+
/**
|
|
98
|
+
* Whether this tool changes anything outside the session. Undeclared means
|
|
99
|
+
* write, so a dry-run session answers it instead of running it.
|
|
100
|
+
*/
|
|
101
|
+
effect?: ToolEffectDeclaration<ToolInput<TSchema>>;
|
|
102
|
+
/** What a `"write"` tool returns in a dry-run session. */
|
|
103
|
+
dryRunResult?:
|
|
104
|
+
| ToolExecuteResult
|
|
105
|
+
| ((input: ToolInput<TSchema>) => ToolExecuteResult);
|
|
94
106
|
execute: (
|
|
95
107
|
input: ToolInput<TSchema>,
|
|
96
108
|
ctx: ToolContext
|
|
@@ -112,6 +124,13 @@ type AgentToolConfig<TSchema> = {
|
|
|
112
124
|
execute?: never;
|
|
113
125
|
/** Human-in-the-loop approvals are only supported for server tools. */
|
|
114
126
|
needsApproval?: never;
|
|
127
|
+
/**
|
|
128
|
+
* Agent tools run inside the harness, not through the host's tool path, so
|
|
129
|
+
* a dry run cannot answer them. Classify the work as a server tool if it
|
|
130
|
+
* needs to be dry-runnable.
|
|
131
|
+
*/
|
|
132
|
+
effect?: never;
|
|
133
|
+
dryRunResult?: never;
|
|
115
134
|
};
|
|
116
135
|
|
|
117
136
|
/**
|
package/src/types.ts
CHANGED
|
@@ -867,6 +867,35 @@ export type ToolNeedsApproval<TInput> =
|
|
|
867
867
|
| boolean
|
|
868
868
|
| ((input: TInput) => boolean | Promise<boolean>);
|
|
869
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Whether invoking a tool can change anything outside this session.
|
|
872
|
+
*
|
|
873
|
+
* `"read"` cannot, by construction — a query, a file read, a listing.
|
|
874
|
+
* `"write"` changes something a person or another agent will later see: an
|
|
875
|
+
* external system (a comment, a message, a ticket) or shared storage another
|
|
876
|
+
* session reads back.
|
|
877
|
+
*
|
|
878
|
+
* This is what {@link SendMessageOptions.dryRun} enforces. In a dry-run
|
|
879
|
+
* session a write is answered by the host and never executed, while reads run
|
|
880
|
+
* for real — so an agent can be run against real data to see what it *would*
|
|
881
|
+
* have done.
|
|
882
|
+
*
|
|
883
|
+
* A tool that declares nothing is treated as a write: a dry run promises
|
|
884
|
+
* "nothing outside this session changed", which it cannot promise about a tool
|
|
885
|
+
* it knows nothing about. An unannotated tool is therefore useless in a dry
|
|
886
|
+
* run.
|
|
887
|
+
*/
|
|
888
|
+
export type ToolEffect = "read" | "write";
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* A tool's declared {@link ToolEffect} — fixed, or decided per call from
|
|
892
|
+
* validated input for tools that read or write depending on their arguments
|
|
893
|
+
* (an HTTP request keyed on method, a SQL tool keyed on statement).
|
|
894
|
+
*/
|
|
895
|
+
export type ToolEffectDeclaration<TInput> =
|
|
896
|
+
| ToolEffect
|
|
897
|
+
| ((input: TInput) => ToolEffect);
|
|
898
|
+
|
|
870
899
|
export interface ToolConfig<TInput> {
|
|
871
900
|
description: string;
|
|
872
901
|
/**
|
|
@@ -895,6 +924,20 @@ export interface ToolConfig<TInput> {
|
|
|
895
924
|
* `execution: "server"` tools on the local runtime.
|
|
896
925
|
*/
|
|
897
926
|
needsApproval?: ToolNeedsApproval<TInput>;
|
|
927
|
+
/**
|
|
928
|
+
* Whether this tool changes anything outside the session. Undeclared means
|
|
929
|
+
* write, so a dry run stubs it — see {@link ToolEffect}.
|
|
930
|
+
*/
|
|
931
|
+
effect?: ToolEffectDeclaration<TInput>;
|
|
932
|
+
/**
|
|
933
|
+
* What a write tool returns in a dry-run session instead of executing.
|
|
934
|
+
*
|
|
935
|
+
* Omit for a tool whose result the caller only acknowledges. Supply one when
|
|
936
|
+
* the result is read structurally — a caller that uses `result.revision`
|
|
937
|
+
* breaks on a bare string — so a dry run stays on the same path production
|
|
938
|
+
* takes. Ignored unless {@link effect} resolves to `"write"`.
|
|
939
|
+
*/
|
|
940
|
+
dryRunResult?: ToolExecuteResult | ((input: TInput) => ToolExecuteResult);
|
|
898
941
|
/**
|
|
899
942
|
* In-process implementation. Required when {@link execution} is
|
|
900
943
|
* `"server"` (the default); ignored for `"agent"` tools.
|
|
@@ -918,6 +961,10 @@ export interface ToolDefinition extends BrandedDefinition<"tool"> {
|
|
|
918
961
|
readonly inputSchema?: z.ZodTypeAny | JsonSchemaObject;
|
|
919
962
|
readonly outputSchema?: z.ZodTypeAny | JsonSchemaObject;
|
|
920
963
|
readonly needsApproval?: ToolNeedsApproval<never>;
|
|
964
|
+
readonly effect?: ToolEffectDeclaration<never>;
|
|
965
|
+
readonly dryRunResult?:
|
|
966
|
+
| ToolExecuteResult
|
|
967
|
+
| ((input: never) => ToolExecuteResult);
|
|
921
968
|
readonly execute?: (
|
|
922
969
|
input: never,
|
|
923
970
|
ctx: ToolContext
|
|
@@ -1349,6 +1396,13 @@ export type SessionEventPayload =
|
|
|
1349
1396
|
toolName: string;
|
|
1350
1397
|
output?: unknown;
|
|
1351
1398
|
isError: boolean;
|
|
1399
|
+
/**
|
|
1400
|
+
* The tool never ran: a write answered by the host in a `dryRun`
|
|
1401
|
+
* session. `actions.requested` still carries the arguments the model
|
|
1402
|
+
* chose, so this is the marker that separates "what it would have
|
|
1403
|
+
* done" from a real effect when reading a trajectory.
|
|
1404
|
+
*/
|
|
1405
|
+
stubbed?: true;
|
|
1352
1406
|
parentCallId?: string;
|
|
1353
1407
|
};
|
|
1354
1408
|
}
|
|
@@ -1451,6 +1505,8 @@ export interface SessionInfo {
|
|
|
1451
1505
|
mode: SessionMode;
|
|
1452
1506
|
/** Omit / `"live"` for production traffic; `"eval"` for eval harness sessions. */
|
|
1453
1507
|
purpose?: SessionPurpose;
|
|
1508
|
+
/** True when write-classified tool calls are answered by the host. */
|
|
1509
|
+
dryRun?: boolean;
|
|
1454
1510
|
auth: AuthContext | null;
|
|
1455
1511
|
title?: string;
|
|
1456
1512
|
/**
|
|
@@ -1483,6 +1539,8 @@ export interface SessionRecord {
|
|
|
1483
1539
|
* Older records omit this (treated as `"live"`).
|
|
1484
1540
|
*/
|
|
1485
1541
|
purpose?: SessionPurpose;
|
|
1542
|
+
/** Frozen at create from {@link SendMessageOptions.dryRun}. */
|
|
1543
|
+
dryRun?: boolean;
|
|
1486
1544
|
/**
|
|
1487
1545
|
* Denormalized sticky A/B arms (mirrors durable `ab.assigned` events) so
|
|
1488
1546
|
* the agent loop / tools can read arms without re-folding the log. Source
|
|
@@ -1657,6 +1715,15 @@ export interface SendMessageOptions {
|
|
|
1657
1715
|
* (`defineAB`) so regression runs do not mix into live metrics.
|
|
1658
1716
|
*/
|
|
1659
1717
|
purpose?: SessionPurpose;
|
|
1718
|
+
/**
|
|
1719
|
+
* Answer every write-classified tool call from the host instead of running
|
|
1720
|
+
* it, for this session's whole life (create only — a follow-up cannot clear
|
|
1721
|
+
* it, so a session that starts as a rehearsal stays one).
|
|
1722
|
+
*
|
|
1723
|
+
* Reads execute, writes are answered and recorded, and the arguments the
|
|
1724
|
+
* model chose stay on the session log as the record of the intended action.
|
|
1725
|
+
*/
|
|
1726
|
+
dryRun?: boolean;
|
|
1660
1727
|
/**
|
|
1661
1728
|
* Absolute directory used as the local harness `cwd` for this session.
|
|
1662
1729
|
* On create, sets the session workspace. On resume (continuation hit),
|
|
@@ -2506,6 +2573,12 @@ export interface DiscoveredTool {
|
|
|
2506
2573
|
* predicate). Runtime evaluation still runs the predicate per call.
|
|
2507
2574
|
*/
|
|
2508
2575
|
needsApproval: boolean;
|
|
2576
|
+
/**
|
|
2577
|
+
* The tool's declared {@link ToolEffect} when it declares one statically, or
|
|
2578
|
+
* `"dynamic"` when it declares a predicate resolved per call. Absent when it
|
|
2579
|
+
* declares nothing, which a dry run treats as a write.
|
|
2580
|
+
*/
|
|
2581
|
+
effect?: ToolEffect | "dynamic";
|
|
2509
2582
|
/**
|
|
2510
2583
|
* Agent tools: script body materialized under
|
|
2511
2584
|
* `.agent-serve/tools/<name>.sh`.
|