@cursor/july 0.1.83 → 0.1.85
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/channels/deployments/deployments-channel.d.ts +2 -2
- package/dist/channels/deployments/deployments-channel.d.ts.map +1 -1
- package/dist/channels/deployments/deployments-channel.js +7 -6
- package/dist/channels/deployments/index.d.ts +2 -3
- package/dist/channels/deployments/index.d.ts.map +1 -1
- package/dist/channels/deployments/index.js +2 -3
- package/dist/channels/deployments/types.d.ts +14 -14
- package/dist/channels/deployments/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.6THHGRH-.js → app.DUOPbN18.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BpHB9ulP.js → VPLocalSearchBox.CmWbGcGk.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DBJvJznv.js → theme.S57OeOLA.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.G5GysZ7e.js → reference_channels.md.CboFd5IH.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +5 -5
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cursor-deployment-relay.d.ts +4 -4
- package/dist/internal/cursor-deployment-relay.d.ts.map +1 -1
- package/dist/internal/cursor-deployment-relay.js +33 -32
- 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/server.js +13 -13
- 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/playground/assets/index-CMe7imuc.css +1 -0
- package/dist/playground/assets/{index-Cspgk9jC.js → index-JaFLlugp.js} +45 -45
- package/dist/playground/index.html +2 -2
- 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/docs/reference/channels.md +13 -11
- package/package.json +1 -1
- package/src/channels/deployments/deployments-channel.ts +7 -6
- package/src/channels/deployments/index.ts +2 -3
- package/src/channels/deployments/types.ts +14 -14
- package/src/internal/cursor-deployment-relay.ts +38 -36
- 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/server.ts +13 -13
- 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
- package/dist/playground/assets/index-ChETdG-_.css +0 -1
- /package/dist/docs/assets/{reference_channels.md.G5GysZ7e.lean.js → reference_channels.md.CboFd5IH.lean.js} +0 -0
|
@@ -54,6 +54,7 @@ import { sessionExternalLinks } from "./session-links.js";
|
|
|
54
54
|
import { SessionStore } from "./session-store.js";
|
|
55
55
|
import { syncSkillsToStore } from "./skills-sync.js";
|
|
56
56
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
57
|
+
import { decideToolCall } from "./tool-policy.js";
|
|
57
58
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
58
59
|
import { estimateTurnCostFromApi, roundCostUsd, } from "./turn-cost.js";
|
|
59
60
|
import { TurnGovernor } from "./turn-governor.js";
|
|
@@ -124,6 +125,15 @@ export class SessionEngine {
|
|
|
124
125
|
this.activeTurns = new Map();
|
|
125
126
|
/** Context used by the cloud turn's HTTP MCP callbacks. */
|
|
126
127
|
this.activeToolContexts = new Map();
|
|
128
|
+
/**
|
|
129
|
+
* Tool call ids the host answered instead of running, per session.
|
|
130
|
+
*
|
|
131
|
+
* The mapper emits the result event from the harness's view of a finished
|
|
132
|
+
* call, which cannot tell an answered call from a real one — so the decision
|
|
133
|
+
* point records the id here and the mapper stamps the event from it.
|
|
134
|
+
* Registered only while a turn can invoke a tool.
|
|
135
|
+
*/
|
|
136
|
+
this.stubbedCallIds = new Map();
|
|
127
137
|
/**
|
|
128
138
|
* Delivered-nudge count per session at the last tool boundary. The carrier
|
|
129
139
|
* drains out-of-process, so a growing count is how the engine learns a
|
|
@@ -530,24 +540,7 @@ export class SessionEngine {
|
|
|
530
540
|
const now = new Date().toISOString();
|
|
531
541
|
const channelState = this.initialChannelState(channelId, sendOptions.state);
|
|
532
542
|
const purpose = sendOptions.purpose === "eval" ? "eval" : "live";
|
|
533
|
-
let record = {
|
|
534
|
-
sessionId: newSessionId(),
|
|
535
|
-
channelId,
|
|
536
|
-
mode: (_d = sendOptions.mode) !== null && _d !== void 0 ? _d : "chat",
|
|
537
|
-
purpose,
|
|
538
|
-
continuationKey: (_e = sendOptions.continuationToken) !== null && _e !== void 0 ? _e : newContinuationKey(),
|
|
539
|
-
auth,
|
|
540
|
-
title: sendOptions.title,
|
|
541
|
-
channelState,
|
|
542
|
-
workspaceOverride: sendOptions.workspaceDir,
|
|
543
|
-
pendingWorkspaceFiles: sendOptions.workspaceFiles,
|
|
544
|
-
pendingImages: sendOptions.images,
|
|
545
|
-
cloudOverride: sendOptions.cloud,
|
|
546
|
-
sdkAgentId: sendOptions.sdkAgentId,
|
|
547
|
-
turnCount: 0,
|
|
548
|
-
createdAt: now,
|
|
549
|
-
updatedAt: now,
|
|
550
|
-
};
|
|
543
|
+
let record = Object.assign(Object.assign({ sessionId: newSessionId(), channelId, mode: (_d = sendOptions.mode) !== null && _d !== void 0 ? _d : "chat", purpose }, (sendOptions.dryRun === true ? { dryRun: true } : {})), { continuationKey: (_e = sendOptions.continuationToken) !== null && _e !== void 0 ? _e : newContinuationKey(), auth, title: sendOptions.title, channelState, workspaceOverride: sendOptions.workspaceDir, pendingWorkspaceFiles: sendOptions.workspaceFiles, pendingImages: sendOptions.images, cloudOverride: sendOptions.cloud, sdkAgentId: sendOptions.sdkAgentId, turnCount: 0, createdAt: now, updatedAt: now });
|
|
551
544
|
yield this.sessions.save(record);
|
|
552
545
|
(_a = this.storage) === null || _a === void 0 ? void 0 : _a.sessionRecord(record);
|
|
553
546
|
yield this.appendEvent(record.sessionId, {
|
|
@@ -1130,7 +1123,10 @@ export class SessionEngine {
|
|
|
1130
1123
|
this.logger(`[agent-sdk] failed to record event for ${sessionId}: ${describeError(error)}`);
|
|
1131
1124
|
}
|
|
1132
1125
|
};
|
|
1133
|
-
|
|
1126
|
+
// Read by closure, so the mapper still sees it while draining after the
|
|
1127
|
+
// registry entry below is gone.
|
|
1128
|
+
const stubbed = new Set();
|
|
1129
|
+
const mapper = new TurnEventMapper(emit, (callId) => stubbed.has(callId));
|
|
1134
1130
|
// Take a turn slot before any work happens. When every slot is busy the
|
|
1135
1131
|
// turn waits FIFO — durable `turn.queued` marks the wait — and a
|
|
1136
1132
|
// preempting follow-up (or interrupt) resolves the wait as cancelled
|
|
@@ -1214,11 +1210,13 @@ export class SessionEngine {
|
|
|
1214
1210
|
}
|
|
1215
1211
|
}), signal, pendingImages);
|
|
1216
1212
|
this.activeToolContexts.set(sessionId, { record, turnId, emit });
|
|
1213
|
+
this.stubbedCallIds.set(sessionId, stubbed);
|
|
1217
1214
|
try {
|
|
1218
1215
|
outcome = yield this.runner.runTurn(request);
|
|
1219
1216
|
}
|
|
1220
1217
|
finally {
|
|
1221
1218
|
this.activeToolContexts.delete(sessionId);
|
|
1219
|
+
this.stubbedCallIds.delete(sessionId);
|
|
1222
1220
|
}
|
|
1223
1221
|
}
|
|
1224
1222
|
catch (error) {
|
|
@@ -1752,6 +1750,7 @@ export class SessionEngine {
|
|
|
1752
1750
|
executeServerTool(args) {
|
|
1753
1751
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1754
1752
|
var _a;
|
|
1753
|
+
var _b;
|
|
1755
1754
|
const execute = args.tool.definition.execute;
|
|
1756
1755
|
if (args.tool.execution !== "server" || typeof execute !== "function") {
|
|
1757
1756
|
return toolErrorResult(`Tool "${args.tool.name}" is not a server tool.`);
|
|
@@ -1760,7 +1759,7 @@ export class SessionEngine {
|
|
|
1760
1759
|
if (!validation.ok) {
|
|
1761
1760
|
return toolErrorResult(`Invalid input for tool "${args.tool.name}": ${validation.message}`);
|
|
1762
1761
|
}
|
|
1763
|
-
const toolCallId = (
|
|
1762
|
+
const toolCallId = (_b = args.toolCallId) !== null && _b !== void 0 ? _b : newToolCallId(args.tool.name);
|
|
1764
1763
|
const toolCtx = {
|
|
1765
1764
|
toolCallId,
|
|
1766
1765
|
session: this.sessionInfo(args.record),
|
|
@@ -1776,6 +1775,19 @@ export class SessionEngine {
|
|
|
1776
1775
|
emit: args.emit,
|
|
1777
1776
|
};
|
|
1778
1777
|
try {
|
|
1778
|
+
// Before the approval gate: a dry run is unattended, so parking a write
|
|
1779
|
+
// for a human would hang the turn rather than answer it.
|
|
1780
|
+
const decision = decideToolCall({
|
|
1781
|
+
dryRun: args.record.dryRun === true,
|
|
1782
|
+
effect: args.tool.definition.effect,
|
|
1783
|
+
dryRunResult: args.tool.definition.dryRunResult,
|
|
1784
|
+
input: validation.value,
|
|
1785
|
+
});
|
|
1786
|
+
if (decision.kind === "answer") {
|
|
1787
|
+
(_a = this.stubbedCallIds.get(args.record.sessionId)) === null || _a === void 0 ? void 0 : _a.add(toolCallId);
|
|
1788
|
+
this.logger(`[session] tool ${args.tool.name} answered by host (${toolCallId})`);
|
|
1789
|
+
return normalizeToolResult(decision.result);
|
|
1790
|
+
}
|
|
1779
1791
|
const needsGate = yield evaluateNeedsApproval(args.tool.definition.needsApproval, validation.value);
|
|
1780
1792
|
if (needsGate) {
|
|
1781
1793
|
const decision = yield this.parkForApproval({
|
|
@@ -1994,10 +2006,20 @@ export class SessionEngine {
|
|
|
1994
2006
|
data: { calls: [{ callId, toolName, args: validation.value }] },
|
|
1995
2007
|
});
|
|
1996
2008
|
}
|
|
2009
|
+
// A separate body from the model path, so it asks the same policy: a
|
|
2010
|
+
// session-bound `POST /v1/tools/:name`, `call --session`, or
|
|
2011
|
+
// `handle.callTool` performs a write just as readily.
|
|
2012
|
+
const decision = decideToolCall({
|
|
2013
|
+
dryRun: (record === null || record === void 0 ? void 0 : record.dryRun) === true,
|
|
2014
|
+
effect: tool.definition.effect,
|
|
2015
|
+
dryRunResult: tool.definition.dryRunResult,
|
|
2016
|
+
input: validation.value,
|
|
2017
|
+
});
|
|
1997
2018
|
const started = Date.now();
|
|
1998
2019
|
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
1999
2020
|
let result;
|
|
2000
2021
|
let isError;
|
|
2022
|
+
const stubbed = decision.kind === "answer";
|
|
2001
2023
|
const live = record;
|
|
2002
2024
|
const emit = live === undefined
|
|
2003
2025
|
? undefined
|
|
@@ -2005,26 +2027,33 @@ export class SessionEngine {
|
|
|
2005
2027
|
void this.appendEvent(live.sessionId, Object.assign(Object.assign({}, payload), { turnId: callId })).catch(() => { });
|
|
2006
2028
|
};
|
|
2007
2029
|
try {
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
?
|
|
2023
|
-
:
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2030
|
+
if (decision.kind === "answer") {
|
|
2031
|
+
this.logger(`[session] tool ${toolName} answered by host (direct ${callId})`);
|
|
2032
|
+
result = normalizeToolResult(decision.result);
|
|
2033
|
+
isError = false;
|
|
2034
|
+
}
|
|
2035
|
+
else {
|
|
2036
|
+
result = normalizeToolResult(yield execute(validation.value, {
|
|
2037
|
+
toolCallId: callId,
|
|
2038
|
+
session,
|
|
2039
|
+
workspaceDir,
|
|
2040
|
+
stateRoot: this.stateRoot,
|
|
2041
|
+
host: this.hostForSession(record === null || record === void 0 ? void 0 : record.sessionId),
|
|
2042
|
+
// Scratch calls have no real session; their facade stays unbound.
|
|
2043
|
+
artifacts: record === undefined
|
|
2044
|
+
? this.artifactsApi()
|
|
2045
|
+
: this.artifactsApi({
|
|
2046
|
+
sessionId: record.sessionId,
|
|
2047
|
+
turnId: callId,
|
|
2048
|
+
}),
|
|
2049
|
+
send: (channelId, message, sendOptions = {}) => this.send(channelId, message, Object.assign(Object.assign({}, sendOptions), { auth: sendOptions.auth !== undefined
|
|
2050
|
+
? sendOptions.auth
|
|
2051
|
+
: session.auth })),
|
|
2052
|
+
getSession: (channelId, sessionId) => this.getChannelSession(channelId, sessionId),
|
|
2053
|
+
emit,
|
|
2054
|
+
}));
|
|
2055
|
+
isError = isErrorEnvelope(result);
|
|
2056
|
+
}
|
|
2028
2057
|
}
|
|
2029
2058
|
catch (error) {
|
|
2030
2059
|
this.logger(`[session] tool ${toolName} failed (direct ${callId}): ${describeError(error)}`);
|
|
@@ -2051,7 +2080,8 @@ export class SessionEngine {
|
|
|
2051
2080
|
yield this.appendEvent(record.sessionId, {
|
|
2052
2081
|
type: "action.result",
|
|
2053
2082
|
turnId: callId,
|
|
2054
|
-
data: { callId,
|
|
2083
|
+
data: Object.assign({ callId,
|
|
2084
|
+
toolName, output: result, isError }, (stubbed ? { stubbed: true } : {})),
|
|
2055
2085
|
});
|
|
2056
2086
|
}
|
|
2057
2087
|
const errorMessage = isError ? toolCallErrorMessage(result) : undefined;
|
|
@@ -2062,6 +2092,33 @@ export class SessionEngine {
|
|
|
2062
2092
|
durationMs }, (errorMessage === undefined ? {} : { errorMessage })), (record === undefined ? {} : { sessionId: record.sessionId }));
|
|
2063
2093
|
});
|
|
2064
2094
|
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Whether a dry run answers a bridged MCP call, for the cloud host bridge —
|
|
2097
|
+
* the one tool path that does not reach {@link executeServerTool}, because
|
|
2098
|
+
* it forwards to `host.mcp` instead of invoking a tool body.
|
|
2099
|
+
*
|
|
2100
|
+
* A bridged tool carries no `defineTool`, so it declares no effect, and the
|
|
2101
|
+
* policy reads an undeclared tool as a write. In a dry run that answers
|
|
2102
|
+
* bridged reads too; resolving effects from a server's own listing is the
|
|
2103
|
+
* next unit's work.
|
|
2104
|
+
*/
|
|
2105
|
+
bridgedCallAnswer(args) {
|
|
2106
|
+
var _a;
|
|
2107
|
+
// Read off the in-flight turn's context rather than the session store:
|
|
2108
|
+
// the bridge already refuses a call with no active turn, so the record is
|
|
2109
|
+
// in memory. That keeps this free for the sessions that are not dry runs,
|
|
2110
|
+
// which is every session today.
|
|
2111
|
+
const record = (_a = this.activeToolContexts.get(args.sessionId)) === null || _a === void 0 ? void 0 : _a.record;
|
|
2112
|
+
const decision = decideToolCall({
|
|
2113
|
+
dryRun: (record === null || record === void 0 ? void 0 : record.dryRun) === true,
|
|
2114
|
+
effect: undefined,
|
|
2115
|
+
});
|
|
2116
|
+
if (decision.kind === "run") {
|
|
2117
|
+
return { answered: false };
|
|
2118
|
+
}
|
|
2119
|
+
this.logger(`[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`);
|
|
2120
|
+
return { answered: true, result: normalizeToolResult(decision.result) };
|
|
2121
|
+
}
|
|
2065
2122
|
// ==========================================================================
|
|
2066
2123
|
// Events: append, dispatch to channel handlers + hooks
|
|
2067
2124
|
// ==========================================================================
|
|
@@ -2609,7 +2666,7 @@ export class SessionEngine {
|
|
|
2609
2666
|
this.coalesceDrains.has(sessionId));
|
|
2610
2667
|
}
|
|
2611
2668
|
sessionInfo(record) {
|
|
2612
|
-
return Object.assign(Object.assign(Object.assign({ id: record.sessionId, channelId: record.channelId, mode: record.mode, purpose: record.purpose === "eval" ? "eval" : "live", auth: record.auth, continuationKey: record.continuationKey }, (record.abs === undefined ? {} : { abs: record.abs })), (record.title === undefined ? {} : { title: record.title })), (record.sdkAgentId === undefined
|
|
2669
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: record.sessionId, channelId: record.channelId, mode: record.mode, purpose: record.purpose === "eval" ? "eval" : "live" }, (record.dryRun === true ? { dryRun: true } : {})), { auth: record.auth, continuationKey: record.continuationKey }), (record.abs === undefined ? {} : { abs: record.abs })), (record.title === undefined ? {} : { title: record.title })), (record.sdkAgentId === undefined
|
|
2613
2670
|
? {}
|
|
2614
2671
|
: { sdkAgentId: record.sdkAgentId }));
|
|
2615
2672
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import type { ToolEffect, ToolEffectDeclaration, ToolExecuteResult } from "../types.js";
|
|
12
|
+
/** What the host does with one tool call. */
|
|
13
|
+
export type ToolDecision =
|
|
14
|
+
/** Run the tool body. */
|
|
15
|
+
{
|
|
16
|
+
kind: "run";
|
|
17
|
+
}
|
|
18
|
+
/** Do not run it; return this to the model instead. */
|
|
19
|
+
| {
|
|
20
|
+
kind: "answer";
|
|
21
|
+
result: ToolExecuteResult;
|
|
22
|
+
};
|
|
23
|
+
/** What a policy decision needs to know about the call. */
|
|
24
|
+
export interface ToolCallFacts {
|
|
25
|
+
/** The session answers writes instead of running them. */
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
/** The tool's declaration, if it made one. */
|
|
28
|
+
effect: ToolEffectDeclaration<never> | undefined;
|
|
29
|
+
/** What a stubbed write should answer with, if the tool supplied one. */
|
|
30
|
+
dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
|
|
31
|
+
/**
|
|
32
|
+
* Validated input, as the tool body would receive it. Only read by a
|
|
33
|
+
* per-call `effect` predicate and by a `dryRunResult` function, so a caller
|
|
34
|
+
* with neither — a bridged MCP tool, which carries no declaration at all —
|
|
35
|
+
* can omit it.
|
|
36
|
+
*/
|
|
37
|
+
input?: unknown;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The effect of one specific call.
|
|
41
|
+
*
|
|
42
|
+
* Undeclared resolves to `"write"`. A dry run promises that nothing outside
|
|
43
|
+
* the session changed, and it cannot promise that about a tool it knows
|
|
44
|
+
* nothing about — so the unknown case is the one that costs a stubbed read
|
|
45
|
+
* rather than the one that leaks a real write.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveEffect(effect: ToolEffectDeclaration<never> | undefined, input: unknown): ToolEffect;
|
|
48
|
+
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
49
|
+
export declare function decideToolCall(facts: ToolCallFacts): ToolDecision;
|
|
50
|
+
//# sourceMappingURL=tool-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-policy.d.ts","sourceRoot":"","sources":["../../src/internal/tool-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAErB,6CAA6C;AAC7C,MAAM,MAAM,YAAY;AACtB,yBAAyB;AACvB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE;AACjB,uDAAuD;GACrD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAElD,2DAA2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;IACjD,yEAAyE;IACzE,YAAY,CAAC,EAAE,iBAAiB,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,iBAAiB,CAAC,CAAC;IACzE;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAUD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,SAAS,EAChD,KAAK,EAAE,OAAO,GACb,UAAU,CAIZ;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAWjE"}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
* What a stubbed write answers. Deliberately not self-describing: a model told
|
|
13
|
+
* mid-turn that its writes are not real starts hedging its conclusions, and
|
|
14
|
+
* the conclusion is the thing a dry run exists to read. Tell the agent once,
|
|
15
|
+
* in its opening prompt, not in a tool result.
|
|
16
|
+
*/
|
|
17
|
+
const ACKNOWLEDGED = "Operation acknowledged.";
|
|
18
|
+
/**
|
|
19
|
+
* The effect of one specific call.
|
|
20
|
+
*
|
|
21
|
+
* Undeclared resolves to `"write"`. A dry run promises that nothing outside
|
|
22
|
+
* the session changed, and it cannot promise that about a tool it knows
|
|
23
|
+
* nothing about — so the unknown case is the one that costs a stubbed read
|
|
24
|
+
* rather than the one that leaks a real write.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveEffect(effect, input) {
|
|
27
|
+
const declared = typeof effect === "function" ? effect(input) : effect;
|
|
28
|
+
return declared === "read" ? "read" : "write";
|
|
29
|
+
}
|
|
30
|
+
/** Decide what to do with one tool call. Pure; no I/O, no session state. */
|
|
31
|
+
export function decideToolCall(facts) {
|
|
32
|
+
if (!facts.dryRun) {
|
|
33
|
+
return { kind: "run" };
|
|
34
|
+
}
|
|
35
|
+
if (resolveEffect(facts.effect, facts.input) === "read") {
|
|
36
|
+
return { kind: "run" };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
kind: "answer",
|
|
40
|
+
result: dryRunAnswer(facts.dryRunResult, facts.input),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function dryRunAnswer(dryRunResult, input) {
|
|
44
|
+
if (dryRunResult === undefined) {
|
|
45
|
+
return ACKNOWLEDGED;
|
|
46
|
+
}
|
|
47
|
+
if (typeof dryRunResult === "function") {
|
|
48
|
+
return dryRunResult(input);
|
|
49
|
+
}
|
|
50
|
+
return dryRunResult;
|
|
51
|
+
}
|