@datename/fastcode-termux 0.1.6-beta → 0.1.6
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/README.md +6 -24
- package/dist/agent/guards.d.ts +0 -8
- package/dist/agent/guards.js +0 -195
- package/dist/agent/protocol.js +1 -4
- package/dist/agent/tools.d.ts +1 -3
- package/dist/agent/tools.js +377 -2
- package/dist/app.js +103 -335
- package/dist/cli.js +311 -362
- package/dist/commands/index.js +0 -22
- package/dist/commands/login.d.ts +2 -4
- package/dist/commands/login.js +10 -12
- package/dist/commands/logout.js +16 -8
- package/dist/commands/model.d.ts +0 -13
- package/dist/commands/model.js +22 -42
- package/dist/commands/provider.d.ts +0 -13
- package/dist/commands/provider.js +9 -20
- package/dist/commands/whoami.js +5 -6
- package/dist/config/store.d.ts +12 -32
- package/dist/config/store.js +65 -117
- package/dist/config/trust.d.ts +8 -21
- package/dist/config/trust.js +13 -99
- package/dist/lib/diagnostics.d.ts +4 -29
- package/dist/lib/diagnostics.js +4 -60
- package/dist/lib/http.d.ts +1 -11
- package/dist/lib/http.js +4 -62
- package/dist/providers/anthropic.d.ts +0 -1
- package/dist/providers/anthropic.js +0 -12
- package/dist/providers/custom.d.ts +0 -9
- package/dist/providers/deepseek.d.ts +0 -10
- package/dist/providers/deepseek.js +1 -2
- package/dist/providers/google.d.ts +0 -9
- package/dist/providers/groq.d.ts +0 -10
- package/dist/providers/groq.js +1 -2
- package/dist/providers/index.d.ts +201 -10
- package/dist/providers/index.js +0 -30
- package/dist/providers/mistral.d.ts +0 -10
- package/dist/providers/mistral.js +1 -2
- package/dist/providers/moonshot.d.ts +0 -10
- package/dist/providers/moonshot.js +1 -2
- package/dist/providers/ollama.d.ts +0 -9
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +1 -2
- package/dist/providers/openaiCompatible.d.ts +0 -9
- package/dist/providers/openaiCompatible.js +10 -58
- package/dist/providers/openrouter.d.ts +0 -10
- package/dist/providers/openrouter.js +1 -2
- package/dist/providers/qwen.d.ts +0 -10
- package/dist/providers/qwen.js +1 -2
- package/dist/providers/xai.d.ts +0 -10
- package/dist/providers/xai.js +1 -2
- package/dist/providers/zhipu.d.ts +0 -10
- package/dist/providers/zhipu.js +1 -2
- package/dist/session/log.d.ts +12 -35
- package/dist/session/log.js +10 -206
- package/dist/types.d.ts +1 -60
- package/dist/ui/Message.d.ts +7 -1
- package/dist/ui/Message.js +2 -13
- package/dist/ui/Prompt.d.ts +2 -1
- package/dist/ui/Prompt.js +2 -2
- package/dist/ui/Spinner.js +5 -11
- package/dist/ui/StatusBar.d.ts +1 -2
- package/dist/ui/StatusBar.js +5 -6
- package/package.json +1 -1
- package/src/agent/guards.ts +0 -196
- package/src/agent/protocol.ts +1 -4
- package/src/agent/tools.ts +377 -3
- package/src/app.ts +103 -335
- package/src/cli.ts +3 -30
- package/src/commands/index.ts +0 -22
- package/src/commands/login.ts +10 -12
- package/src/commands/logout.ts +16 -8
- package/src/commands/model.ts +22 -42
- package/src/commands/provider.ts +9 -20
- package/src/commands/whoami.ts +5 -6
- package/src/config/store.ts +69 -122
- package/src/config/trust.ts +21 -137
- package/src/lib/diagnostics.ts +4 -60
- package/src/lib/http.ts +4 -66
- package/src/providers/anthropic.ts +1 -13
- package/src/providers/deepseek.ts +1 -2
- package/src/providers/groq.ts +1 -2
- package/src/providers/index.ts +1 -32
- package/src/providers/mistral.ts +1 -2
- package/src/providers/moonshot.ts +1 -2
- package/src/providers/openai.ts +1 -2
- package/src/providers/openaiCompatible.ts +10 -58
- package/src/providers/openrouter.ts +1 -2
- package/src/providers/qwen.ts +1 -2
- package/src/providers/xai.ts +1 -2
- package/src/providers/zhipu.ts +1 -2
- package/src/session/log.ts +28 -267
- package/src/types.ts +1 -63
- package/src/ui/Message.ts +2 -13
- package/src/ui/Prompt.ts +2 -2
- package/src/ui/Spinner.ts +5 -11
- package/src/ui/StatusBar.ts +5 -6
- package/dist/agent/approval.d.ts +0 -50
- package/dist/agent/approval.js +0 -7
- package/dist/agent/compaction.d.ts +0 -39
- package/dist/agent/compaction.js +0 -227
- package/dist/agent/context.d.ts +0 -29
- package/dist/agent/context.js +0 -62
- package/dist/agent/ignore.d.ts +0 -11
- package/dist/agent/ignore.js +0 -129
- package/dist/agent/process-tree.d.ts +0 -2
- package/dist/agent/process-tree.js +0 -31
- package/dist/agent/protocol-mode.d.ts +0 -6
- package/dist/agent/protocol-mode.js +0 -51
- package/dist/agent/registry.d.ts +0 -17
- package/dist/agent/registry.js +0 -90
- package/dist/agent/runner.d.ts +0 -106
- package/dist/agent/runner.js +0 -352
- package/dist/agent/shell-session.d.ts +0 -37
- package/dist/agent/shell-session.js +0 -400
- package/dist/agent/stream.d.ts +0 -29
- package/dist/agent/stream.js +0 -103
- package/dist/agent/tool-schemas.d.ts +0 -2
- package/dist/agent/tool-schemas.js +0 -63
- package/dist/agent/tools/append_file.d.ts +0 -2
- package/dist/agent/tools/append_file.js +0 -26
- package/dist/agent/tools/apply_patch.d.ts +0 -10
- package/dist/agent/tools/apply_patch.js +0 -52
- package/dist/agent/tools/copy_file.d.ts +0 -2
- package/dist/agent/tools/copy_file.js +0 -30
- package/dist/agent/tools/delete_file.d.ts +0 -2
- package/dist/agent/tools/delete_file.js +0 -24
- package/dist/agent/tools/grep.d.ts +0 -2
- package/dist/agent/tools/grep.js +0 -23
- package/dist/agent/tools/index.d.ts +0 -2
- package/dist/agent/tools/index.js +0 -32
- package/dist/agent/tools/list_files.d.ts +0 -2
- package/dist/agent/tools/list_files.js +0 -17
- package/dist/agent/tools/make_dir.d.ts +0 -2
- package/dist/agent/tools/make_dir.js +0 -24
- package/dist/agent/tools/move_path.d.ts +0 -2
- package/dist/agent/tools/move_path.js +0 -47
- package/dist/agent/tools/paths.d.ts +0 -7
- package/dist/agent/tools/paths.js +0 -84
- package/dist/agent/tools/pwd.d.ts +0 -2
- package/dist/agent/tools/pwd.js +0 -10
- package/dist/agent/tools/read_file.d.ts +0 -2
- package/dist/agent/tools/read_file.js +0 -21
- package/dist/agent/tools/read_many.d.ts +0 -2
- package/dist/agent/tools/read_many.js +0 -25
- package/dist/agent/tools/readers.d.ts +0 -6
- package/dist/agent/tools/readers.js +0 -216
- package/dist/agent/tools/replace_in_file.d.ts +0 -2
- package/dist/agent/tools/replace_in_file.js +0 -30
- package/dist/agent/tools/shell.d.ts +0 -2
- package/dist/agent/tools/shell.js +0 -48
- package/dist/agent/tools/stat_path.d.ts +0 -2
- package/dist/agent/tools/stat_path.js +0 -31
- package/dist/agent/tools/types.d.ts +0 -39
- package/dist/agent/tools/types.js +0 -1
- package/dist/agent/tools/validate.d.ts +0 -6
- package/dist/agent/tools/validate.js +0 -29
- package/dist/agent/tools/write_file.d.ts +0 -2
- package/dist/agent/tools/write_file.js +0 -26
- package/dist/commands/attach.d.ts +0 -9
- package/dist/commands/attach.js +0 -15
- package/dist/commands/bg.d.ts +0 -7
- package/dist/commands/bg.js +0 -9
- package/dist/commands/cancel.d.ts +0 -10
- package/dist/commands/cancel.js +0 -10
- package/dist/commands/compact.d.ts +0 -10
- package/dist/commands/compact.js +0 -22
- package/dist/commands/context.d.ts +0 -7
- package/dist/commands/context.js +0 -24
- package/dist/commands/edit.d.ts +0 -10
- package/dist/commands/edit.js +0 -36
- package/dist/commands/effort.d.ts +0 -25
- package/dist/commands/effort.js +0 -48
- package/dist/commands/expand.d.ts +0 -10
- package/dist/commands/expand.js +0 -8
- package/dist/commands/jobs.d.ts +0 -7
- package/dist/commands/jobs.js +0 -16
- package/dist/commands/kill.d.ts +0 -7
- package/dist/commands/kill.js +0 -11
- package/dist/commands/steer.d.ts +0 -11
- package/dist/commands/steer.js +0 -13
- package/dist/lib/model-meta.d.ts +0 -19
- package/dist/lib/model-meta.js +0 -113
- package/dist/lib/native-tools.d.ts +0 -12
- package/dist/lib/native-tools.js +0 -21
- package/dist/lib/net.d.ts +0 -33
- package/dist/lib/net.js +0 -168
- package/dist/patch/apply.d.ts +0 -15
- package/dist/patch/apply.js +0 -172
- package/dist/patch/parse.d.ts +0 -29
- package/dist/patch/parse.js +0 -112
- package/dist/patch/plan.d.ts +0 -49
- package/dist/patch/plan.js +0 -273
- package/dist/session/projection.d.ts +0 -43
- package/dist/session/projection.js +0 -257
- package/dist/ui/Picker.d.ts +0 -8
- package/dist/ui/Picker.js +0 -24
- package/src/agent/approval.ts +0 -47
- package/src/agent/compaction.ts +0 -276
- package/src/agent/context.ts +0 -97
- package/src/agent/ignore.ts +0 -147
- package/src/agent/process-tree.ts +0 -32
- package/src/agent/protocol-mode.ts +0 -59
- package/src/agent/registry.ts +0 -98
- package/src/agent/runner.ts +0 -443
- package/src/agent/shell-session.ts +0 -449
- package/src/agent/stream.ts +0 -143
- package/src/agent/tool-schemas.ts +0 -67
- package/src/agent/tools/append_file.ts +0 -28
- package/src/agent/tools/apply_patch.ts +0 -54
- package/src/agent/tools/copy_file.ts +0 -32
- package/src/agent/tools/delete_file.ts +0 -26
- package/src/agent/tools/grep.ts +0 -25
- package/src/agent/tools/index.ts +0 -34
- package/src/agent/tools/list_files.ts +0 -19
- package/src/agent/tools/make_dir.ts +0 -26
- package/src/agent/tools/move_path.ts +0 -50
- package/src/agent/tools/paths.ts +0 -92
- package/src/agent/tools/pwd.ts +0 -12
- package/src/agent/tools/read_file.ts +0 -23
- package/src/agent/tools/read_many.ts +0 -27
- package/src/agent/tools/readers.ts +0 -228
- package/src/agent/tools/replace_in_file.ts +0 -32
- package/src/agent/tools/shell.ts +0 -53
- package/src/agent/tools/stat_path.ts +0 -33
- package/src/agent/tools/types.ts +0 -42
- package/src/agent/tools/validate.ts +0 -35
- package/src/agent/tools/write_file.ts +0 -28
- package/src/commands/attach.ts +0 -16
- package/src/commands/bg.ts +0 -9
- package/src/commands/cancel.ts +0 -10
- package/src/commands/compact.ts +0 -23
- package/src/commands/context.ts +0 -25
- package/src/commands/edit.ts +0 -37
- package/src/commands/effort.ts +0 -49
- package/src/commands/expand.ts +0 -8
- package/src/commands/jobs.ts +0 -17
- package/src/commands/kill.ts +0 -12
- package/src/commands/steer.ts +0 -13
- package/src/lib/model-meta.ts +0 -138
- package/src/lib/native-tools.ts +0 -32
- package/src/lib/net.ts +0 -204
- package/src/patch/apply.ts +0 -194
- package/src/patch/parse.ts +0 -131
- package/src/patch/plan.ts +0 -317
- package/src/session/projection.ts +0 -308
- package/src/ui/Picker.ts +0 -24
package/src/agent/registry.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { isTrusted } from "../config/trust.js";
|
|
3
|
-
import type { ApprovalDecision } from "./approval.js";
|
|
4
|
-
import { evaluateShellCommand } from "./guards.js";
|
|
5
|
-
import { TOOLS } from "./tools/index.js";
|
|
6
|
-
import type { RevalidateKey, StateScope, Tool, ToolCall, ToolCtx, ToolResult } from "./tools/types.js";
|
|
7
|
-
|
|
8
|
-
const STATE_SCOPES = new Set<StateScope>(["call", "turn", "session"]);
|
|
9
|
-
const REVALIDATE_KEYS = new Set<RevalidateKey>(["trust", "sandboxRoots", "schema", "modelCaps"]);
|
|
10
|
-
|
|
11
|
-
export function assertToolContract(tools: Tool[] = TOOLS): void {
|
|
12
|
-
const names = new Set<string>();
|
|
13
|
-
for (const tool of tools) {
|
|
14
|
-
if (!tool.name || typeof tool.name !== "string")
|
|
15
|
-
throw new Error("tool contract: every tool needs a name");
|
|
16
|
-
if (names.has(tool.name))
|
|
17
|
-
throw new Error(`tool contract: duplicate tool name ${tool.name}`);
|
|
18
|
-
names.add(tool.name);
|
|
19
|
-
if (!STATE_SCOPES.has(tool.stateScope))
|
|
20
|
-
throw new Error(`tool contract: ${tool.name} has an invalid stateScope`);
|
|
21
|
-
for (const key of tool.revalidate ?? []) {
|
|
22
|
-
if (!REVALIDATE_KEYS.has(key))
|
|
23
|
-
throw new Error(`tool contract: ${tool.name} has an invalid revalidate key ${String(key)}`);
|
|
24
|
-
}
|
|
25
|
-
if (tool.requiresTrust && !(tool.revalidate ?? []).includes("trust")) {
|
|
26
|
-
throw new Error(`tool contract: ${tool.name} requiresTrust but does not revalidate "trust"`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function toolCatalogHash(tools: Tool[] = TOOLS): string {
|
|
32
|
-
const catalog = tools
|
|
33
|
-
.map((tool) => ({
|
|
34
|
-
name: tool.name,
|
|
35
|
-
trust: Boolean(tool.requiresTrust),
|
|
36
|
-
scope: tool.stateScope,
|
|
37
|
-
revalidate: [...(tool.revalidate ?? [])].sort(),
|
|
38
|
-
}))
|
|
39
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
40
|
-
return createHash("sha1").update(JSON.stringify(catalog)).digest("hex").slice(0, 8);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class ToolRegistry {
|
|
44
|
-
private readonly tools = new Map<string, Tool>();
|
|
45
|
-
constructor(tools: Tool[] = TOOLS) {
|
|
46
|
-
assertToolContract(tools);
|
|
47
|
-
for (const tool of tools)
|
|
48
|
-
this.tools.set(tool.name, tool);
|
|
49
|
-
}
|
|
50
|
-
get(name: string): Tool | undefined {
|
|
51
|
-
return this.tools.get(name);
|
|
52
|
-
}
|
|
53
|
-
list(): Tool[] {
|
|
54
|
-
return Array.from(this.tools.values());
|
|
55
|
-
}
|
|
56
|
-
approve(call: ToolCall, ctx: ToolCtx): ApprovalDecision {
|
|
57
|
-
const tool = this.get(call.tool);
|
|
58
|
-
if (!tool)
|
|
59
|
-
return { kind: "deny", reason: `unknown tool: ${String(call.tool)}` };
|
|
60
|
-
if (tool.name === "shell") {
|
|
61
|
-
const verdict = evaluateShellCommand(call.command);
|
|
62
|
-
if (!verdict.allow)
|
|
63
|
-
return { kind: "deny", reason: verdict.output };
|
|
64
|
-
}
|
|
65
|
-
if ((tool.revalidate ?? []).includes("trust") && !isTrusted(ctx.cwd)) {
|
|
66
|
-
return {
|
|
67
|
-
kind: "deny",
|
|
68
|
-
reason: `project is not trusted: ${ctx.cwd}. Run \`fastcode trust ${ctx.cwd}\` or accept the trust prompt.`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return { kind: "allow", scope: "once" };
|
|
72
|
-
}
|
|
73
|
-
async execute(call: ToolCall, ctx: ToolCtx): Promise<ToolResult> {
|
|
74
|
-
const tool = this.get(call.tool);
|
|
75
|
-
if (!tool)
|
|
76
|
-
return { ok: false, output: `unknown tool: ${String(call.tool)}` };
|
|
77
|
-
const prepared = tool.prepare(call, ctx);
|
|
78
|
-
if (prepared.ok === false)
|
|
79
|
-
return { ok: false, output: prepared.output };
|
|
80
|
-
const decision = this.approve(call, ctx);
|
|
81
|
-
if (decision.kind === "deny")
|
|
82
|
-
return { ok: false, output: decision.reason ?? "denied by approval policy" };
|
|
83
|
-
if (decision.kind === "pending")
|
|
84
|
-
return { ok: false, output: "approval required, but no approval resolver is registered (fail closed)" };
|
|
85
|
-
try {
|
|
86
|
-
return await tool.execute(call, ctx);
|
|
87
|
-
}
|
|
88
|
-
catch (err: any) {
|
|
89
|
-
return { ok: false, output: err?.message ?? String(err) };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const registry = new ToolRegistry();
|
|
95
|
-
|
|
96
|
-
export async function runAgentTool(call: ToolCall, cwd = process.cwd(), options: { signal?: AbortSignal; turn?: number } = {}): Promise<ToolResult> {
|
|
97
|
-
return registry.execute(call, { cwd, signal: options.signal, turn: options.turn });
|
|
98
|
-
}
|
package/src/agent/runner.ts
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync, statSync } from "node:fs";
|
|
3
|
-
import type { ChatChunk, ChatMessage, NativeToolCall, Provider, ReasoningEffort, ToolSchema } from "../types.js";
|
|
4
|
-
import type { ActiveFile } from "../session/log.js";
|
|
5
|
-
import type { SideEffects } from "./tools/types.js";
|
|
6
|
-
import { estMessagesTokens, estTokens, getModelMeta } from "../lib/model-meta.js";
|
|
7
|
-
import { compactMessages, DEFAULT_COMPACTION_CONFIG, type CompactionConfig } from "./compaction.js";
|
|
8
|
-
import { isLoopExempt, isReadOnlyToolCall, toolCallFingerprint } from "./guards.js";
|
|
9
|
-
import { isNativeUnsupportedError, type ToolProtocol } from "./protocol-mode.js";
|
|
10
|
-
import { formatToolResults, parseToolCalls, stripToolCalls } from "./protocol.js";
|
|
11
|
-
import { runStream } from "./stream.js";
|
|
12
|
-
import { runAgentTool } from "./tools.js";
|
|
13
|
-
|
|
14
|
-
export interface ToolCall {
|
|
15
|
-
tool: string;
|
|
16
|
-
[key: string]: any;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface LoopDetectionOptions {
|
|
20
|
-
readOnlyThreshold?: number;
|
|
21
|
-
sideEffectThreshold?: number;
|
|
22
|
-
window?: number;
|
|
23
|
-
exemptReadOnly?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RunOptions {
|
|
27
|
-
provider: Provider;
|
|
28
|
-
apiKey?: string;
|
|
29
|
-
model: string;
|
|
30
|
-
messages?: ChatMessage[];
|
|
31
|
-
prompt: string;
|
|
32
|
-
system?: string;
|
|
33
|
-
maxTurns?: number;
|
|
34
|
-
allowTools?: boolean;
|
|
35
|
-
tokenBudget?: number;
|
|
36
|
-
costBudgetUsd?: number;
|
|
37
|
-
signal?: AbortSignal;
|
|
38
|
-
cwd?: string;
|
|
39
|
-
idleTimeoutMs?: number;
|
|
40
|
-
contextBudgetChars?: number;
|
|
41
|
-
compaction?: CompactionConfig;
|
|
42
|
-
contextWindow?: number;
|
|
43
|
-
serverOpaqueState?: string;
|
|
44
|
-
loopDetection?: LoopDetectionOptions;
|
|
45
|
-
reasoningEffort?: ReasoningEffort;
|
|
46
|
-
protocol?: ToolProtocol;
|
|
47
|
-
tools?: ToolSchema[];
|
|
48
|
-
onProtocolFallback?: (message: string) => void;
|
|
49
|
-
onNativeOk?: () => void;
|
|
50
|
-
takeSteering?: () => string[];
|
|
51
|
-
onDelta?: (delta: string, text: string) => void;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export type AgentStopReason =
|
|
55
|
-
| "complete"
|
|
56
|
-
| "max_turns"
|
|
57
|
-
| "token_budget"
|
|
58
|
-
| "cost_budget"
|
|
59
|
-
| "loop_detected"
|
|
60
|
-
| "aborted"
|
|
61
|
-
| "incomplete"
|
|
62
|
-
| "stream_error";
|
|
63
|
-
|
|
64
|
-
export type AgentErrorKind = "token_budget" | "cost_budget" | "loop_detected" | "aborted" | "stream_error";
|
|
65
|
-
|
|
66
|
-
export type AgentEvent =
|
|
67
|
-
| {
|
|
68
|
-
type: "compaction";
|
|
69
|
-
beforeChars: number;
|
|
70
|
-
afterChars: number;
|
|
71
|
-
beforeTokens: number;
|
|
72
|
-
afterTokens: number;
|
|
73
|
-
droppedMessages: number;
|
|
74
|
-
keptMessages: number;
|
|
75
|
-
reason: "threshold" | "heap";
|
|
76
|
-
source: "local" | "server";
|
|
77
|
-
summaryFile?: string;
|
|
78
|
-
}
|
|
79
|
-
| { type: "assistant_raw"; text: string; toolCalls?: NativeToolCall[] }
|
|
80
|
-
| { type: "assistant_text"; text: string }
|
|
81
|
-
| { type: "tool_call"; id: string; call: ToolCall; native?: boolean }
|
|
82
|
-
| { type: "tool_result"; id: string; call: ToolCall; ok: boolean; output: string; sideEffects?: SideEffects }
|
|
83
|
-
| { type: "usage"; usage: NonNullable<ChatChunk["usage"]> }
|
|
84
|
-
| { type: "loop_detected"; fingerprint: string; occurrences: number; warned: boolean }
|
|
85
|
-
| { type: "notice"; text: string }
|
|
86
|
-
| { type: "error"; kind: AgentErrorKind; message: string; text?: string; used?: number }
|
|
87
|
-
| { type: "checkpoint"; turn: number; cwd: string; providerId: string; model: string; activeFiles: ActiveFile[] }
|
|
88
|
-
| { type: "done"; history: ChatMessage[]; finalText: string; stopReason: AgentStopReason };
|
|
89
|
-
|
|
90
|
-
export const LOOP_WARNING = "[loop detection] You appear to be repeating the same tool call. " +
|
|
91
|
-
"Do not repeat it again: inspect the result you already have, or change approach.";
|
|
92
|
-
|
|
93
|
-
interface LoopVerdict {
|
|
94
|
-
action: "ok" | "warn" | "stop";
|
|
95
|
-
occurrences: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Sliding-window loop detector.
|
|
100
|
-
*
|
|
101
|
-
* Semantics (fixed by FC-004, do not re-interpret):
|
|
102
|
-
* - hits count the last `window` tool calls, not turns;
|
|
103
|
-
* - read-only tools use a higher threshold than side-effecting tools (D-02);
|
|
104
|
-
* - the first threshold hit yields a warning and resets that fingerprint's count;
|
|
105
|
-
* - only a second full accumulation yields a terminal error;
|
|
106
|
-
* - the table lives for one run (turn), so a new user task starts clean;
|
|
107
|
-
* - fingerprints are normalized per tool call (absolute paths, timeoutMs removed).
|
|
108
|
-
*/
|
|
109
|
-
class LoopState {
|
|
110
|
-
private entries = new Map<string, { warned: boolean; hits: number[] }>();
|
|
111
|
-
private calls = 0;
|
|
112
|
-
private readonly readOnlyThreshold: number;
|
|
113
|
-
private readonly sideEffectThreshold: number;
|
|
114
|
-
private readonly window: number;
|
|
115
|
-
private readonly exemptReadOnly: boolean;
|
|
116
|
-
constructor(options: LoopDetectionOptions = {}) {
|
|
117
|
-
this.readOnlyThreshold = positive(options.readOnlyThreshold, 6);
|
|
118
|
-
this.sideEffectThreshold = positive(options.sideEffectThreshold, 3);
|
|
119
|
-
this.window = positive(options.window, 5);
|
|
120
|
-
this.exemptReadOnly = options.exemptReadOnly === true;
|
|
121
|
-
}
|
|
122
|
-
check(fingerprint: string, readOnly: boolean): LoopVerdict {
|
|
123
|
-
if (readOnly && this.exemptReadOnly)
|
|
124
|
-
return { action: "ok", occurrences: 0 };
|
|
125
|
-
this.calls += 1;
|
|
126
|
-
const index = this.calls;
|
|
127
|
-
let entry = this.entries.get(fingerprint);
|
|
128
|
-
if (!entry) {
|
|
129
|
-
entry = { warned: false, hits: [] };
|
|
130
|
-
this.entries.set(fingerprint, entry);
|
|
131
|
-
}
|
|
132
|
-
entry.hits = entry.hits.filter((hit) => index - hit < this.window);
|
|
133
|
-
entry.hits.push(index);
|
|
134
|
-
const threshold = readOnly ? this.readOnlyThreshold : this.sideEffectThreshold;
|
|
135
|
-
if (entry.hits.length < threshold)
|
|
136
|
-
return { action: "ok", occurrences: entry.hits.length };
|
|
137
|
-
const occurrences = entry.hits.length;
|
|
138
|
-
if (!entry.warned) {
|
|
139
|
-
entry.warned = true;
|
|
140
|
-
entry.hits = [];
|
|
141
|
-
return { action: "warn", occurrences };
|
|
142
|
-
}
|
|
143
|
-
return { action: "stop", occurrences };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function* runAgent(options: RunOptions): AsyncGenerator<AgentEvent, void, void> {
|
|
148
|
-
const provider = options.provider;
|
|
149
|
-
const model = options.model;
|
|
150
|
-
const cwd = options.cwd ?? process.cwd();
|
|
151
|
-
const signal = options.signal;
|
|
152
|
-
const maxTurns = Math.max(1, Math.floor(positive(options.maxTurns, 8)));
|
|
153
|
-
const tokenBudget = optionalPositive(options.tokenBudget);
|
|
154
|
-
const costBudget = optionalPositive(options.costBudgetUsd);
|
|
155
|
-
const loop = new LoopState(options.loopDetection);
|
|
156
|
-
let history: ChatMessage[] = [...(options.messages ?? []), { role: "user", content: options.prompt }];
|
|
157
|
-
let currentText = "";
|
|
158
|
-
let lastVisible = "";
|
|
159
|
-
let consumedInput = 0;
|
|
160
|
-
let spentCost = 0;
|
|
161
|
-
let stopReason: AgentStopReason = "complete";
|
|
162
|
-
let errorMessage = "";
|
|
163
|
-
let naturalFinish = false;
|
|
164
|
-
let toolCallSeq = 0;
|
|
165
|
-
let protocol: ToolProtocol = options.protocol ?? "legacy";
|
|
166
|
-
let fellBack = false;
|
|
167
|
-
const activeFiles = new Map<string, ActiveFile>();
|
|
168
|
-
for (let turn = 0; turn < maxTurns; turn += 1) {
|
|
169
|
-
if (signal?.aborted) {
|
|
170
|
-
stopReason = "aborted";
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
for (const steering of options.takeSteering?.() ?? []) {
|
|
174
|
-
history = [...history, { role: "system", content: "[user steering] " + steering }];
|
|
175
|
-
yield { type: "notice", text: "Steering applied to the next request." };
|
|
176
|
-
}
|
|
177
|
-
const contextWindow = options.contextWindow ?? getModelMeta(provider.id, model).contextWindow;
|
|
178
|
-
const compacted = compactMessages(history, options.compaction ?? DEFAULT_COMPACTION_CONFIG, {
|
|
179
|
-
contextWindow,
|
|
180
|
-
providerId: provider.id,
|
|
181
|
-
model,
|
|
182
|
-
serverOpaqueState: options.serverOpaqueState,
|
|
183
|
-
});
|
|
184
|
-
if (compacted.compacted) {
|
|
185
|
-
history = compacted.messages;
|
|
186
|
-
yield {
|
|
187
|
-
type: "compaction",
|
|
188
|
-
beforeChars: compacted.beforeChars,
|
|
189
|
-
afterChars: compacted.afterChars,
|
|
190
|
-
beforeTokens: compacted.beforeTokens,
|
|
191
|
-
afterTokens: compacted.afterTokens,
|
|
192
|
-
droppedMessages: compacted.droppedMessages,
|
|
193
|
-
keptMessages: compacted.keptMessages,
|
|
194
|
-
reason: compacted.reason ?? "threshold",
|
|
195
|
-
source: compacted.source === "server" ? "server" : "local",
|
|
196
|
-
summaryFile: compacted.summaryFile,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
const promptTokens = estMessagesTokens(history, { providerId: provider.id, model }) +
|
|
200
|
-
estTokens(options.system ?? "", { providerId: provider.id, model });
|
|
201
|
-
if (tokenBudget !== undefined && consumedInput + promptTokens > tokenBudget) {
|
|
202
|
-
stopReason = "token_budget";
|
|
203
|
-
errorMessage = `Token budget exhausted (${consumedInput + promptTokens} > ${tokenBudget} estimated input tokens). Partial output is kept.`;
|
|
204
|
-
yield { type: "error", kind: "token_budget", message: errorMessage, text: lastVisible, used: consumedInput + promptTokens };
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
if (costBudget !== undefined && spentCost >= costBudget) {
|
|
208
|
-
stopReason = "cost_budget";
|
|
209
|
-
errorMessage = `Cost budget exhausted ($${spentCost.toFixed(4)} of $${costBudget}). Partial output is kept.`;
|
|
210
|
-
yield { type: "error", kind: "cost_budget", message: errorMessage, text: lastVisible };
|
|
211
|
-
break;
|
|
212
|
-
}
|
|
213
|
-
currentText = "";
|
|
214
|
-
let resetNotice = "";
|
|
215
|
-
let streamed;
|
|
216
|
-
try {
|
|
217
|
-
streamed = await runStream(provider, {
|
|
218
|
-
model,
|
|
219
|
-
messages: history,
|
|
220
|
-
system: options.system,
|
|
221
|
-
reasoningEffort: options.reasoningEffort,
|
|
222
|
-
...(protocol === "native" && options.tools && options.tools.length > 0 ? { tools: options.tools } : {}),
|
|
223
|
-
}, options.apiKey, {
|
|
224
|
-
signal,
|
|
225
|
-
idleTimeoutMs: options.idleTimeoutMs,
|
|
226
|
-
onDelta: (delta, text) => {
|
|
227
|
-
currentText = text;
|
|
228
|
-
options.onDelta?.(delta, text);
|
|
229
|
-
},
|
|
230
|
-
onReset: () => {
|
|
231
|
-
resetNotice = "Retried stream returned different content; the earlier partial output was discarded and replaced by the new stream.";
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch (err) {
|
|
236
|
-
if (err?.name === "AbortError" || signal?.aborted) {
|
|
237
|
-
stopReason = "aborted";
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
errorMessage = String(err?.message ?? err);
|
|
241
|
-
if (protocol === "native" && !fellBack && isNativeUnsupportedError(errorMessage)) {
|
|
242
|
-
fellBack = true;
|
|
243
|
-
protocol = "legacy";
|
|
244
|
-
options.onProtocolFallback?.(`Native tools were rejected (${errorMessage.slice(0, 160)}); retrying with the pseudo protocol.`);
|
|
245
|
-
turn -= 1;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
stopReason = "stream_error";
|
|
249
|
-
const partial = stripToolCalls(String((err as { partialText?: string })?.partialText ?? "")).trim();
|
|
250
|
-
if (partial)
|
|
251
|
-
lastVisible = partial;
|
|
252
|
-
yield { type: "error", kind: "stream_error", message: errorMessage, text: partial || undefined };
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
if (signal?.aborted) {
|
|
256
|
-
stopReason = "aborted";
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
if (resetNotice)
|
|
260
|
-
yield { type: "notice", text: resetNotice };
|
|
261
|
-
if (streamed.usage) {
|
|
262
|
-
consumedInput += streamed.usage.inputTokens ?? promptTokens;
|
|
263
|
-
spentCost += streamed.usage.costUsd ?? 0;
|
|
264
|
-
yield { type: "usage", usage: streamed.usage };
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
consumedInput += promptTokens;
|
|
268
|
-
}
|
|
269
|
-
if (streamed.incomplete) {
|
|
270
|
-
const partial = stripToolCalls(streamed.text).trim();
|
|
271
|
-
if (partial)
|
|
272
|
-
lastVisible = partial;
|
|
273
|
-
stopReason = "incomplete";
|
|
274
|
-
errorMessage = `Connection ended before the response finished${streamed.retried > 0 ? ` (retried ${streamed.retried}×)` : ""}. ` +
|
|
275
|
-
"Partial output is kept — type 继续 to continue.";
|
|
276
|
-
yield { type: "notice", text: errorMessage };
|
|
277
|
-
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
const visibleText = stripToolCalls(streamed.text);
|
|
281
|
-
if (visibleText)
|
|
282
|
-
lastVisible = visibleText;
|
|
283
|
-
const nativeCalls: NativeToolCall[] = protocol === "native" && options.allowTools !== false ? (streamed.toolCalls ?? []) : [];
|
|
284
|
-
if (nativeCalls.length > 0) {
|
|
285
|
-
yield { type: "assistant_raw", text: streamed.text, toolCalls: nativeCalls };
|
|
286
|
-
if (visibleText)
|
|
287
|
-
yield { type: "assistant_text", text: visibleText };
|
|
288
|
-
history = [...history, { role: "assistant", content: streamed.text, toolCalls: nativeCalls }];
|
|
289
|
-
for (const native of nativeCalls) {
|
|
290
|
-
if (signal?.aborted) {
|
|
291
|
-
stopReason = "aborted";
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
const id = native.id || `tc_${++toolCallSeq}`;
|
|
295
|
-
let call: ToolCall;
|
|
296
|
-
let parseError = "";
|
|
297
|
-
try {
|
|
298
|
-
const args = native.arguments ? JSON.parse(native.arguments) : {};
|
|
299
|
-
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
300
|
-
throw new Error("arguments must be a JSON object");
|
|
301
|
-
call = { ...args, tool: native.name };
|
|
302
|
-
}
|
|
303
|
-
catch (err: any) {
|
|
304
|
-
parseError = `invalid arguments for ${native.name}: ${err?.message ?? String(err)}`;
|
|
305
|
-
call = { tool: native.name };
|
|
306
|
-
}
|
|
307
|
-
const fingerprint = toolCallFingerprint(call, cwd);
|
|
308
|
-
if (!isLoopExempt(call.tool)) {
|
|
309
|
-
const verdict = loop.check(fingerprint, isReadOnlyToolCall(call));
|
|
310
|
-
if (verdict.action === "warn") {
|
|
311
|
-
yield { type: "loop_detected", fingerprint, occurrences: verdict.occurrences, warned: false };
|
|
312
|
-
history = [...history, { role: "system", content: LOOP_WARNING }];
|
|
313
|
-
}
|
|
314
|
-
else if (verdict.action === "stop") {
|
|
315
|
-
stopReason = "loop_detected";
|
|
316
|
-
errorMessage = `Loop detected: the same tool call repeated ${verdict.occurrences} times. Stopped before executing it again.`;
|
|
317
|
-
yield { type: "error", kind: "loop_detected", message: errorMessage, text: lastVisible };
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
yield { type: "tool_call", id, call, native: true };
|
|
322
|
-
const result: any = parseError
|
|
323
|
-
? { ok: false, output: parseError }
|
|
324
|
-
: await runAgentTool(call, cwd, { signal, turn: turn + 1 });
|
|
325
|
-
trackSideEffects(activeFiles, result.sideEffects);
|
|
326
|
-
yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), sideEffects: result.sideEffects };
|
|
327
|
-
history = [...history, { role: "tool", toolCallId: id, content: String(result.output ?? "") }];
|
|
328
|
-
}
|
|
329
|
-
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
330
|
-
if (stopReason === "complete")
|
|
331
|
-
options.onNativeOk?.();
|
|
332
|
-
if (stopReason !== "complete")
|
|
333
|
-
break;
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
const calls: ToolCall[] = options.allowTools === false ? [] : parseToolCalls(streamed.text);
|
|
337
|
-
yield { type: "assistant_raw", text: streamed.text };
|
|
338
|
-
if (calls.length === 0) {
|
|
339
|
-
lastVisible = visibleText || streamed.text;
|
|
340
|
-
naturalFinish = true;
|
|
341
|
-
stopReason = "complete";
|
|
342
|
-
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
history = [...history, { role: "assistant", content: streamed.text }];
|
|
346
|
-
if (visibleText)
|
|
347
|
-
yield { type: "assistant_text", text: visibleText };
|
|
348
|
-
const results: Array<{ call: ToolCall; ok: boolean; output: string; sideEffects?: SideEffects }> = [];
|
|
349
|
-
for (const call of calls) {
|
|
350
|
-
if (signal?.aborted) {
|
|
351
|
-
stopReason = "aborted";
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
const fingerprint = toolCallFingerprint(call, cwd);
|
|
355
|
-
if (!isLoopExempt(call.tool)) {
|
|
356
|
-
const verdict = loop.check(fingerprint, isReadOnlyToolCall(call));
|
|
357
|
-
if (verdict.action === "warn") {
|
|
358
|
-
yield { type: "loop_detected", fingerprint, occurrences: verdict.occurrences, warned: false };
|
|
359
|
-
history = [...history, { role: "system", content: LOOP_WARNING }];
|
|
360
|
-
}
|
|
361
|
-
else if (verdict.action === "stop") {
|
|
362
|
-
stopReason = "loop_detected";
|
|
363
|
-
errorMessage = `Loop detected: the same tool call repeated ${verdict.occurrences} times. Stopped before executing it again.`;
|
|
364
|
-
yield { type: "error", kind: "loop_detected", message: errorMessage, text: lastVisible };
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const id = `tc_${++toolCallSeq}`;
|
|
369
|
-
yield { type: "tool_call", id, call };
|
|
370
|
-
const result: any = await runAgentTool(call, cwd, { signal, turn: turn + 1 });
|
|
371
|
-
results.push({ call, ok: Boolean(result.ok), output: String(result.output ?? ""), sideEffects: result.sideEffects });
|
|
372
|
-
trackSideEffects(activeFiles, result.sideEffects);
|
|
373
|
-
yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), sideEffects: result.sideEffects };
|
|
374
|
-
}
|
|
375
|
-
if (results.length > 0)
|
|
376
|
-
history = [...history, { role: "user", content: formatToolResults(results) }];
|
|
377
|
-
yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
|
|
378
|
-
if (stopReason !== "complete")
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
if (stopReason === "stream_error") {
|
|
382
|
-
const partial = lastVisible;
|
|
383
|
-
if (partial)
|
|
384
|
-
history = [...history, { role: "assistant", content: partial }];
|
|
385
|
-
yield { type: "done", history, finalText: partial, stopReason };
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (stopReason === "complete" && !naturalFinish)
|
|
389
|
-
stopReason = "max_turns";
|
|
390
|
-
let finalText = "";
|
|
391
|
-
if (stopReason === "aborted") {
|
|
392
|
-
const partial = stripToolCalls(currentText).trim() || lastVisible;
|
|
393
|
-
finalText = (partial ? partial + "\n\n" : "") + "[用户中断]";
|
|
394
|
-
yield { type: "error", kind: "aborted", message: "Cancelled.", text: partial };
|
|
395
|
-
}
|
|
396
|
-
else if (stopReason === "token_budget" || stopReason === "cost_budget" || stopReason === "loop_detected" || stopReason === "incomplete") {
|
|
397
|
-
finalText = lastVisible || errorMessage;
|
|
398
|
-
}
|
|
399
|
-
else if (stopReason === "max_turns") {
|
|
400
|
-
finalText = `Reached the maximum of ${maxTurns} agent tool turns. The work so far is kept in this session — type 继续 to keep going.`;
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
finalText = lastVisible;
|
|
404
|
-
}
|
|
405
|
-
if (finalText)
|
|
406
|
-
history = [...history, { role: "assistant", content: finalText }];
|
|
407
|
-
yield { type: "done", history, finalText, stopReason };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function positive(value: number | undefined, fallback: number): number {
|
|
411
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function optionalPositive(value: number | undefined): number | undefined {
|
|
415
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function trackSideEffects(activeFiles: Map<string, ActiveFile>, sideEffects: SideEffects | undefined): void {
|
|
419
|
-
if (!sideEffects)
|
|
420
|
-
return;
|
|
421
|
-
for (const file of [...(sideEffects.touched ?? []), ...(sideEffects.created ?? [])]) {
|
|
422
|
-
const meta = describeActiveFile(file);
|
|
423
|
-
if (meta)
|
|
424
|
-
activeFiles.set(file, meta);
|
|
425
|
-
}
|
|
426
|
-
for (const file of sideEffects.removed ?? [])
|
|
427
|
-
activeFiles.delete(file);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function describeActiveFile(file: string): ActiveFile | undefined {
|
|
431
|
-
try {
|
|
432
|
-
const info = statSync(file);
|
|
433
|
-
if (!info.isFile())
|
|
434
|
-
return undefined;
|
|
435
|
-
const sha1_8 = info.size > 8 * 1024 * 1024
|
|
436
|
-
? "large"
|
|
437
|
-
: createHash("sha1").update(readFileSync(file)).digest("hex").slice(0, 8);
|
|
438
|
-
return { path: file, sha1_8, mtime: info.mtime.toISOString() };
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
return undefined;
|
|
442
|
-
}
|
|
443
|
-
}
|