@cjhyy/code-shell-core 0.8.1 → 0.8.2
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/engine/run-tooling.js +4 -0
- package/dist/engine/turn-loop.d.ts +3 -0
- package/dist/engine/turn-loop.js +80 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tool-system/builtin/tool-search.js +44 -2
- package/dist/tool-system/context.d.ts +8 -1
- package/package.json +1 -1
|
@@ -279,5 +279,9 @@ export function assembleRunToolDefs(args) {
|
|
|
279
279
|
const toolDefs = args.runPlanMode
|
|
280
280
|
? profileToolDefs.filter((t) => PLAN_MODE_ALLOWED_TOOLS.has(t.name))
|
|
281
281
|
: profileToolDefs;
|
|
282
|
+
// ToolSearch shares the worker registry across Sessions, but discovery must
|
|
283
|
+
// reflect this exact run's filtered and rewritten surface. Keep the context
|
|
284
|
+
// snapshot in lockstep with the definitions sent to the model.
|
|
285
|
+
toolCtx.searchableToolDefinitions = toolDefs;
|
|
282
286
|
return toolDefs;
|
|
283
287
|
}
|
|
@@ -202,6 +202,9 @@ export declare class TurnLoop {
|
|
|
202
202
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
203
203
|
*/
|
|
204
204
|
private stopBlockCount;
|
|
205
|
+
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
206
|
+
private repeatedToolBatchFingerprint;
|
|
207
|
+
private repeatedToolBatchCount;
|
|
205
208
|
/**
|
|
206
209
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
207
210
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Following Claude Code's po_() pattern:
|
|
5
5
|
* pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
|
|
6
6
|
*/
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { buildAgentDirectionMessage } from "../tool-system/builtin/agent-notifications.js";
|
|
9
9
|
import { newTurnId } from "./turn-state.js";
|
|
10
10
|
import { formatFriendlyError } from "./friendly-error.js";
|
|
@@ -40,6 +40,39 @@ export function toolResultToBlock(result) {
|
|
|
40
40
|
block.is_error = true;
|
|
41
41
|
return block;
|
|
42
42
|
}
|
|
43
|
+
const REPEATED_TOOL_BATCH_LIMIT = 3;
|
|
44
|
+
function canonicalToolValue(value) {
|
|
45
|
+
if (value === null)
|
|
46
|
+
return "null";
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return "undefined";
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return `[${value.map(canonicalToolValue).join(",")}]`;
|
|
51
|
+
if (typeof value === "object") {
|
|
52
|
+
return `{${Object.entries(value)
|
|
53
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
54
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalToolValue(entry)}`)
|
|
55
|
+
.join(",")}}`;
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
function repeatedToolBatchFingerprint(toolCalls, results) {
|
|
60
|
+
// Hash immediately and never log the canonical source: tool results may
|
|
61
|
+
// contain credentials or large media payloads. Call ids are deliberately
|
|
62
|
+
// omitted because providers generate a fresh id for every identical retry.
|
|
63
|
+
return createHash("sha256")
|
|
64
|
+
.update(canonicalToolValue({
|
|
65
|
+
calls: toolCalls.map((call) => ({ toolName: call.toolName, args: call.args })),
|
|
66
|
+
results: results.map((result) => ({
|
|
67
|
+
toolName: result.toolName,
|
|
68
|
+
isError: result.isError === true || Boolean(result.error),
|
|
69
|
+
error: result.error,
|
|
70
|
+
result: result.result,
|
|
71
|
+
contentBlocks: result.contentBlocks,
|
|
72
|
+
})),
|
|
73
|
+
}))
|
|
74
|
+
.digest("hex");
|
|
75
|
+
}
|
|
43
76
|
export class TurnLoop {
|
|
44
77
|
deps;
|
|
45
78
|
config;
|
|
@@ -76,6 +109,9 @@ export class TurnLoop {
|
|
|
76
109
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
77
110
|
*/
|
|
78
111
|
stopBlockCount = 0;
|
|
112
|
+
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
113
|
+
repeatedToolBatchFingerprint;
|
|
114
|
+
repeatedToolBatchCount = 0;
|
|
79
115
|
/**
|
|
80
116
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
81
117
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
|
@@ -1409,6 +1445,49 @@ export class TurnLoop {
|
|
|
1409
1445
|
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
1410
1446
|
}
|
|
1411
1447
|
}
|
|
1448
|
+
const toolBatchFingerprint = repeatedToolBatchFingerprint(toolCalls, results);
|
|
1449
|
+
if (toolBatchFingerprint === this.repeatedToolBatchFingerprint) {
|
|
1450
|
+
this.repeatedToolBatchCount++;
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
this.repeatedToolBatchFingerprint = toolBatchFingerprint;
|
|
1454
|
+
this.repeatedToolBatchCount = 1;
|
|
1455
|
+
}
|
|
1456
|
+
if (this.repeatedToolBatchCount >= REPEATED_TOOL_BATCH_LIMIT) {
|
|
1457
|
+
tlog.warn("turn.repeated_tool_batch_stopped", {
|
|
1458
|
+
cat: "turn",
|
|
1459
|
+
repeatedCount: this.repeatedToolBatchCount,
|
|
1460
|
+
tools: toolCalls.map((call) => call.toolName),
|
|
1461
|
+
});
|
|
1462
|
+
await this.emitHook("on_turn_end", {
|
|
1463
|
+
turnNumber: this.turnCount,
|
|
1464
|
+
hasToolUse: true,
|
|
1465
|
+
toolCallCount: toolCalls.length,
|
|
1466
|
+
});
|
|
1467
|
+
finalText =
|
|
1468
|
+
`检测到同一组工具调用及其结果连续重复 ${REPEATED_TOOL_BATCH_LIMIT} 次,` +
|
|
1469
|
+
"已自动停止,避免继续空转。请调整请求或让 Session 获取新的上下文后再试。";
|
|
1470
|
+
this.deps.transcript.appendMessage("assistant", finalText);
|
|
1471
|
+
messages.push({ role: "assistant", content: finalText });
|
|
1472
|
+
this.config.onStream?.({
|
|
1473
|
+
type: "assistant_message",
|
|
1474
|
+
messageId: assistantMessageId,
|
|
1475
|
+
message: { role: "assistant", content: finalText },
|
|
1476
|
+
});
|
|
1477
|
+
this.finalizeModelTurn();
|
|
1478
|
+
if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
1479
|
+
this.repeatedToolBatchFingerprint = undefined;
|
|
1480
|
+
this.repeatedToolBatchCount = 0;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1484
|
+
return {
|
|
1485
|
+
text: finalText,
|
|
1486
|
+
reason: "completed",
|
|
1487
|
+
messages,
|
|
1488
|
+
completionKind: "limit_stop",
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1412
1491
|
// Hook: turn end
|
|
1413
1492
|
await this.emitHook("on_turn_end", {
|
|
1414
1493
|
turnNumber: this.turnCount,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.8.
|
|
6
|
+
export declare const VERSION = "0.8.2";
|
|
7
7
|
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
|
|
9
9
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.8.
|
|
6
|
+
export const VERSION = "0.8.2";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Engine (primary API) ────────────────────────────────────────
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
import { isRegisteredMcpToolAllowed } from "../mcp-tool-policy.js";
|
|
9
9
|
export const toolSearchToolDef = {
|
|
10
10
|
name: "ToolSearch",
|
|
11
|
-
description: "Search
|
|
11
|
+
description: "Search only the tools available in the current Session context by name or keyword. " +
|
|
12
12
|
"Some tools (especially from MCP servers) are deferred — their full schemas " +
|
|
13
|
-
"are only loaded when you search for them.
|
|
13
|
+
"are only loaded when you search for them. If an exact tool is reported unavailable, " +
|
|
14
|
+
"do not retry unless the Session context changes.",
|
|
14
15
|
inputSchema: {
|
|
15
16
|
type: "object",
|
|
16
17
|
properties: {
|
|
@@ -37,6 +38,18 @@ export async function toolSearchTool(args, ctx) {
|
|
|
37
38
|
// returning the wrong tool set. `|| 5` only caught 0/NaN, not negatives.
|
|
38
39
|
const rawMax = args.max_results;
|
|
39
40
|
const maxResults = Math.min(typeof rawMax === "number" && rawMax > 0 ? rawMax : 5, 20);
|
|
41
|
+
const currentDefinitions = ctx.searchableToolDefinitions;
|
|
42
|
+
if (currentDefinitions) {
|
|
43
|
+
const currentTools = currentDefinitions.map((definition) => definitionToSearchableTool(definition, ctx.toolRegistry));
|
|
44
|
+
if (query.startsWith("select:")) {
|
|
45
|
+
const names = query
|
|
46
|
+
.slice(7)
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((name) => name.trim());
|
|
49
|
+
return matchCurrentExact(currentTools, names);
|
|
50
|
+
}
|
|
51
|
+
return searchCurrentByKeyword(currentTools, query, maxResults);
|
|
52
|
+
}
|
|
40
53
|
// The tool registry is worker-SHARED (B1): it holds MCP tools registered by
|
|
41
54
|
// every session, including servers another project enabled. ToolSearch is a
|
|
42
55
|
// side door around the per-turn toolDefs filter — without this gate it would
|
|
@@ -63,6 +76,32 @@ export async function toolSearchTool(args, ctx) {
|
|
|
63
76
|
// Keyword search
|
|
64
77
|
return searchByKeyword(ctx.toolRegistry, query, maxResults, visible);
|
|
65
78
|
}
|
|
79
|
+
function definitionToSearchableTool(definition, registry) {
|
|
80
|
+
const registered = registry.getTool(definition.name);
|
|
81
|
+
return {
|
|
82
|
+
name: definition.name,
|
|
83
|
+
description: definition.description,
|
|
84
|
+
inputSchema: definition.inputSchema,
|
|
85
|
+
source: registered?.source ?? "builtin",
|
|
86
|
+
...(registered?.serverName ? { serverName: registered.serverName } : {}),
|
|
87
|
+
...(registered?.mcpToolName ? { mcpToolName: registered.mcpToolName } : {}),
|
|
88
|
+
permissionDefault: registered?.permissionDefault ?? "ask",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function matchCurrentExact(tools, names) {
|
|
92
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
93
|
+
return names
|
|
94
|
+
.map((name) => {
|
|
95
|
+
const tool = byName.get(name);
|
|
96
|
+
return tool
|
|
97
|
+
? formatTool(tool)
|
|
98
|
+
: `Tool "${name}" is not available in the current Session context. Do not retry unless the Session context changes.`;
|
|
99
|
+
})
|
|
100
|
+
.join("\n\n---\n\n");
|
|
101
|
+
}
|
|
102
|
+
function searchCurrentByKeyword(tools, query, maxResults) {
|
|
103
|
+
return searchDetailedTools(tools, query, maxResults);
|
|
104
|
+
}
|
|
66
105
|
function matchExact(registry, names, visible) {
|
|
67
106
|
const results = [];
|
|
68
107
|
for (const name of names) {
|
|
@@ -78,6 +117,9 @@ function matchExact(registry, names, visible) {
|
|
|
78
117
|
}
|
|
79
118
|
function searchByKeyword(registry, query, maxResults, visible) {
|
|
80
119
|
const allTools = registry.listToolsDetailed().filter(visible);
|
|
120
|
+
return searchDetailedTools(allTools, query, maxResults);
|
|
121
|
+
}
|
|
122
|
+
function searchDetailedTools(allTools, query, maxResults) {
|
|
81
123
|
const queryLower = query.toLowerCase();
|
|
82
124
|
const keywords = queryLower.split(/\s+/);
|
|
83
125
|
// Score each tool
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Tools that don't need any context (Read/Write/Bash/...) just ignore
|
|
13
13
|
* the second argument; the type is purely additive.
|
|
14
14
|
*/
|
|
15
|
-
import type { LLMConfig, StreamCallback, TokenUsage } from "../types.js";
|
|
15
|
+
import type { LLMConfig, StreamCallback, TokenUsage, ToolDefinition } from "../types.js";
|
|
16
16
|
import type { ModelPool } from "../llm/model-pool.js";
|
|
17
17
|
import type { ToolRegistry } from "./registry.js";
|
|
18
18
|
import type { AgentPresetName } from "../preset/index.js";
|
|
@@ -242,6 +242,13 @@ export interface ToolContext {
|
|
|
242
242
|
modelPool?: ModelPool;
|
|
243
243
|
/** Tool registry (ToolSearch reads this to enumerate available tools). */
|
|
244
244
|
toolRegistry: ToolRegistry;
|
|
245
|
+
/**
|
|
246
|
+
* Final per-turn tool surface after availability guards, feature flags,
|
|
247
|
+
* behavior-profile allowlists, plan-mode filtering, and definition rewrites.
|
|
248
|
+
* ToolSearch must prefer this list over the worker-shared registry so it
|
|
249
|
+
* cannot advertise a tool the current Session cannot actually call.
|
|
250
|
+
*/
|
|
251
|
+
searchableToolDefinitions?: readonly ToolDefinition[];
|
|
245
252
|
/** Opaque services contributed by capability modules, keyed by capability id. */
|
|
246
253
|
capabilityServices?: Readonly<Record<string, unknown>>;
|
|
247
254
|
/**
|
package/package.json
CHANGED