@clinebot/core 0.0.0 → 0.0.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/README.md +1 -1
- package/dist/default-tools/index.d.ts +1 -0
- package/dist/default-tools/model-tool-routing.d.ts +33 -0
- package/dist/default-tools/schemas.d.ts +13 -7
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +220 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +220 -0
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +244 -192
- package/dist/session/default-session-manager.d.ts +3 -1
- package/dist/session/session-host.d.ts +2 -2
- package/dist/session/session-manager.d.ts +8 -0
- package/dist/session/unified-session-persistence-service.d.ts +1 -1
- package/dist/session/utils/helpers.d.ts +11 -0
- package/dist/session/utils/types.d.ts +42 -0
- package/dist/session/utils/usage.d.ts +9 -0
- package/dist/storage/provider-settings-manager.d.ts +2 -0
- package/dist/types/config.d.ts +8 -1
- package/dist/types.d.ts +1 -1
- package/package.json +19 -20
- package/src/default-tools/definitions.test.ts +130 -1
- package/src/default-tools/definitions.ts +6 -2
- package/src/default-tools/executors/editor.ts +10 -9
- package/src/default-tools/executors/file-read.test.ts +1 -1
- package/src/default-tools/executors/file-read.ts +11 -6
- package/src/default-tools/index.ts +5 -0
- package/src/default-tools/model-tool-routing.test.ts +86 -0
- package/src/default-tools/model-tool-routing.ts +132 -0
- package/src/default-tools/schemas.ts +49 -52
- package/src/index.browser.ts +1 -0
- package/src/index.node.ts +1 -0
- package/src/index.ts +41 -2
- package/src/input/file-indexer.ts +28 -2
- package/src/runtime/runtime-builder.test.ts +69 -0
- package/src/runtime/runtime-builder.ts +20 -0
- package/src/runtime/runtime-parity.test.ts +20 -9
- package/src/server/index.ts +40 -1
- package/src/session/default-session-manager.e2e.test.ts +11 -1
- package/src/session/default-session-manager.test.ts +270 -0
- package/src/session/default-session-manager.ts +109 -191
- package/src/session/index.ts +7 -2
- package/src/session/session-host.ts +30 -18
- package/src/session/session-manager.ts +11 -0
- package/src/session/unified-session-persistence-service.ts +11 -5
- package/src/session/utils/helpers.ts +148 -0
- package/src/session/utils/types.ts +46 -0
- package/src/session/utils/usage.ts +32 -0
- package/src/storage/provider-settings-legacy-migration.test.ts +3 -3
- package/src/storage/provider-settings-manager.test.ts +34 -0
- package/src/storage/provider-settings-manager.ts +22 -1
- package/src/types/config.ts +13 -0
- package/src/types.ts +1 -0
- package/dist/index.js +0 -220
|
@@ -7,7 +7,7 @@ import type { CoreSessionEvent } from "../types/events";
|
|
|
7
7
|
import type { SessionRecord } from "../types/sessions";
|
|
8
8
|
import type { RpcCoreSessionService } from "./rpc-session-service";
|
|
9
9
|
import { RuntimeOAuthTokenManager } from "./runtime-oauth-token-manager";
|
|
10
|
-
import type { SendSessionInput, SessionManager, StartSessionInput, StartSessionResult } from "./session-manager";
|
|
10
|
+
import type { SendSessionInput, SessionAccumulatedUsage, SessionManager, StartSessionInput, StartSessionResult } from "./session-manager";
|
|
11
11
|
import type { CoreSessionService } from "./session-service";
|
|
12
12
|
type SessionBackend = CoreSessionService | RpcCoreSessionService;
|
|
13
13
|
export interface DefaultSessionManagerOptions {
|
|
@@ -32,11 +32,13 @@ export declare class DefaultSessionManager implements SessionManager {
|
|
|
32
32
|
private readonly defaultRequestToolApproval?;
|
|
33
33
|
private readonly listeners;
|
|
34
34
|
private readonly sessions;
|
|
35
|
+
private readonly usageBySession;
|
|
35
36
|
constructor(options: DefaultSessionManagerOptions);
|
|
36
37
|
private resolveStoredProviderSettings;
|
|
37
38
|
private buildResolvedProviderConfig;
|
|
38
39
|
start(input: StartSessionInput): Promise<StartSessionResult>;
|
|
39
40
|
send(input: SendSessionInput): Promise<AgentResult | undefined>;
|
|
41
|
+
getAccumulatedUsage(sessionId: string): Promise<SessionAccumulatedUsage | undefined>;
|
|
40
42
|
abort(sessionId: string): Promise<void>;
|
|
41
43
|
stop(sessionId: string): Promise<void>;
|
|
42
44
|
dispose(reason?: string): Promise<void>;
|
|
@@ -3,7 +3,7 @@ import type { ToolExecutors } from "../default-tools";
|
|
|
3
3
|
import { RpcCoreSessionService } from "./rpc-session-service";
|
|
4
4
|
import type { SessionManager } from "./session-manager";
|
|
5
5
|
import { CoreSessionService } from "./session-service";
|
|
6
|
-
type SessionBackend = RpcCoreSessionService | CoreSessionService;
|
|
6
|
+
export type SessionBackend = RpcCoreSessionService | CoreSessionService;
|
|
7
7
|
export interface CreateSessionHostOptions {
|
|
8
8
|
distinctId?: string;
|
|
9
9
|
sessionService?: SessionBackend;
|
|
@@ -17,5 +17,5 @@ export interface CreateSessionHostOptions {
|
|
|
17
17
|
requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult>;
|
|
18
18
|
}
|
|
19
19
|
export type SessionHost = SessionManager;
|
|
20
|
+
export declare function resolveSessionBackend(options: CreateSessionHostOptions): Promise<SessionBackend>;
|
|
20
21
|
export declare function createSessionHost(options: CreateSessionHostOptions): Promise<SessionHost>;
|
|
21
|
-
export {};
|
|
@@ -34,9 +34,17 @@ export interface SendSessionInput {
|
|
|
34
34
|
userImages?: string[];
|
|
35
35
|
userFiles?: string[];
|
|
36
36
|
}
|
|
37
|
+
export interface SessionAccumulatedUsage {
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
cacheReadTokens: number;
|
|
41
|
+
cacheWriteTokens: number;
|
|
42
|
+
totalCost: number;
|
|
43
|
+
}
|
|
37
44
|
export interface SessionManager {
|
|
38
45
|
start(input: StartSessionInput): Promise<StartSessionResult>;
|
|
39
46
|
send(input: SendSessionInput): Promise<AgentResult | undefined>;
|
|
47
|
+
getAccumulatedUsage(sessionId: string): Promise<SessionAccumulatedUsage | undefined>;
|
|
40
48
|
abort(sessionId: string): Promise<void>;
|
|
41
49
|
stop(sessionId: string): Promise<void>;
|
|
42
50
|
dispose(reason?: string): Promise<void>;
|
|
@@ -76,7 +76,7 @@ export declare class UnifiedSessionPersistenceService {
|
|
|
76
76
|
upsertSubagentSessionFromHook(event: HookEventPayload): Promise<string | undefined>;
|
|
77
77
|
appendSubagentHookAudit(subSessionId: string, event: HookEventPayload): Promise<void>;
|
|
78
78
|
appendSubagentTranscriptLine(subSessionId: string, line: string): Promise<void>;
|
|
79
|
-
persistSessionMessages(sessionId: string, messages: LlmsProviders.Message[]): Promise<void>;
|
|
79
|
+
persistSessionMessages(sessionId: string, messages: LlmsProviders.Message[], systemPrompt?: string): Promise<void>;
|
|
80
80
|
applySubagentStatus(subSessionId: string, event: HookEventPayload): Promise<void>;
|
|
81
81
|
applySubagentStatusBySessionId(subSessionId: string, status: SessionStatus): Promise<void>;
|
|
82
82
|
applyStatusToRunningChildSessions(parentSessionId: string, status: Exclude<SessionStatus, "running">): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentConfig, AgentEvent, AgentResult } from "@clinebot/agents";
|
|
2
|
+
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
3
|
+
import type { SessionRecord } from "../../types/sessions";
|
|
4
|
+
import type { SessionRowShape } from "../session-service";
|
|
5
|
+
import type { StoredMessageWithMetadata } from "./types";
|
|
6
|
+
export declare function extractWorkspaceMetadataFromSystemPrompt(systemPrompt: string): string | undefined;
|
|
7
|
+
export declare function hasRuntimeHooks(hooks: AgentConfig["hooks"]): boolean;
|
|
8
|
+
export declare function mergeAgentExtensions(explicitExtensions: AgentConfig["extensions"] | undefined, loadedExtensions: AgentConfig["extensions"] | undefined): AgentConfig["extensions"];
|
|
9
|
+
export declare function serializeAgentEvent(event: AgentEvent): string;
|
|
10
|
+
export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult): StoredMessageWithMetadata[];
|
|
11
|
+
export declare function toSessionRecord(row: SessionRowShape): SessionRecord;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Agent } from "@clinebot/agents";
|
|
2
|
+
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
3
|
+
import type { BuiltRuntime } from "../../runtime/session-runtime";
|
|
4
|
+
import type { SessionSource } from "../../types/common";
|
|
5
|
+
import type { CoreSessionConfig } from "../../types/config";
|
|
6
|
+
import type { SessionAccumulatedUsage } from "../session-manager";
|
|
7
|
+
import type { RootSessionArtifacts } from "../session-service";
|
|
8
|
+
export type ActiveSession = {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
config: CoreSessionConfig;
|
|
11
|
+
artifacts?: RootSessionArtifacts;
|
|
12
|
+
source: SessionSource;
|
|
13
|
+
startedAt: string;
|
|
14
|
+
pendingPrompt?: string;
|
|
15
|
+
runtime: BuiltRuntime;
|
|
16
|
+
agent: Agent;
|
|
17
|
+
started: boolean;
|
|
18
|
+
aborting: boolean;
|
|
19
|
+
interactive: boolean;
|
|
20
|
+
activeTeamRunIds: Set<string>;
|
|
21
|
+
pendingTeamRunUpdates: TeamRunUpdate[];
|
|
22
|
+
teamRunWaiters: Array<() => void>;
|
|
23
|
+
pluginSandboxShutdown?: () => Promise<void>;
|
|
24
|
+
turnUsageBaseline?: SessionAccumulatedUsage;
|
|
25
|
+
};
|
|
26
|
+
export type TeamRunUpdate = {
|
|
27
|
+
runId: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
taskId?: string;
|
|
30
|
+
status: "completed" | "failed" | "cancelled" | "interrupted";
|
|
31
|
+
error?: string;
|
|
32
|
+
iterations?: number;
|
|
33
|
+
};
|
|
34
|
+
export type StoredMessageWithMetadata = LlmsProviders.MessageWithMetadata & {
|
|
35
|
+
providerId?: string;
|
|
36
|
+
modelId?: string;
|
|
37
|
+
};
|
|
38
|
+
export type PreparedTurnInput = {
|
|
39
|
+
prompt: string;
|
|
40
|
+
userImages?: string[];
|
|
41
|
+
userFiles?: string[];
|
|
42
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SessionAccumulatedUsage } from "../session-manager";
|
|
2
|
+
export declare function createInitialAccumulatedUsage(): SessionAccumulatedUsage;
|
|
3
|
+
export declare function accumulateUsageTotals(baseline: SessionAccumulatedUsage, usage: {
|
|
4
|
+
inputTokens?: number;
|
|
5
|
+
outputTokens?: number;
|
|
6
|
+
cacheReadTokens?: number;
|
|
7
|
+
cacheWriteTokens?: number;
|
|
8
|
+
totalCost?: number;
|
|
9
|
+
}): SessionAccumulatedUsage;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ProviderConfig, type ProviderSettings, type ProviderTokenSource, type StoredProviderSettings } from "../types/provider-settings";
|
|
2
2
|
export interface ProviderSettingsManagerOptions {
|
|
3
3
|
filePath?: string;
|
|
4
|
+
dataDir?: string;
|
|
4
5
|
}
|
|
5
6
|
export interface SaveProviderSettingsOptions {
|
|
6
7
|
setLastUsed?: boolean;
|
|
@@ -8,6 +9,7 @@ export interface SaveProviderSettingsOptions {
|
|
|
8
9
|
}
|
|
9
10
|
export declare class ProviderSettingsManager {
|
|
10
11
|
private readonly filePath;
|
|
12
|
+
private readonly dataDir?;
|
|
11
13
|
constructor(options?: ProviderSettingsManagerOptions);
|
|
12
14
|
getFilePath(): string;
|
|
13
15
|
read(): StoredProviderSettings;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { AgentConfig, AgentHooks, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
|
|
1
|
+
import type { AgentConfig, AgentHooks, ConsecutiveMistakeLimitContext, ConsecutiveMistakeLimitDecision, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
|
|
2
2
|
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
3
3
|
import type { AgentMode, BasicLogger, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
|
|
4
|
+
import type { ToolRoutingRule } from "../default-tools/model-tool-routing.js";
|
|
4
5
|
export type CoreAgentMode = AgentMode;
|
|
5
6
|
export interface CoreModelConfig {
|
|
6
7
|
providerId: string;
|
|
@@ -14,6 +15,10 @@ export interface CoreModelConfig {
|
|
|
14
15
|
* Request model-side thinking/reasoning when supported.
|
|
15
16
|
*/
|
|
16
17
|
thinking?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Explicit reasoning effort override for capable models.
|
|
20
|
+
*/
|
|
21
|
+
reasoningEffort?: LlmsProviders.ProviderConfig["reasoningEffort"];
|
|
17
22
|
}
|
|
18
23
|
export interface CoreRuntimeFeatures {
|
|
19
24
|
enableTools: boolean;
|
|
@@ -34,4 +39,6 @@ export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures,
|
|
|
34
39
|
pluginPaths?: string[];
|
|
35
40
|
extensions?: AgentConfig["extensions"];
|
|
36
41
|
onTeamEvent?: (event: TeamEvent) => void;
|
|
42
|
+
onConsecutiveMistakeLimitReached?: (context: ConsecutiveMistakeLimitContext) => Promise<ConsecutiveMistakeLimitDecision> | ConsecutiveMistakeLimitDecision;
|
|
43
|
+
toolRoutingRules?: ToolRoutingRule[];
|
|
37
44
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { SandboxCallOptions, SubprocessSandboxOptions, } from "./runtime/sa
|
|
|
7
7
|
export { SubprocessSandbox } from "./runtime/sandbox/subprocess-sandbox";
|
|
8
8
|
export type { BuiltRuntime as RuntimeEnvironment, RuntimeBuilder, RuntimeBuilderInput, SessionRuntime, } from "./runtime/session-runtime";
|
|
9
9
|
export type { CreateSessionHostOptions, SessionHost, } from "./session/session-host";
|
|
10
|
-
export type { SendSessionInput, SessionManager, StartSessionInput, StartSessionResult, } from "./session/session-manager";
|
|
10
|
+
export type { SendSessionInput, SessionAccumulatedUsage, SessionManager, StartSessionInput, StartSessionResult, } from "./session/session-manager";
|
|
11
11
|
export type { SessionManifest } from "./session/session-manifest";
|
|
12
12
|
export type { CreateRootSessionWithArtifactsInput, RootSessionArtifacts, } from "./session/session-service";
|
|
13
13
|
export type { WorkspaceManager, WorkspaceManagerEvent, } from "./session/workspace-manager";
|
package/package.json
CHANGED
|
@@ -1,45 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinebot/core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"main": "dist/index.js",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"main": "dist/index.node.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@clinebot/agents": "
|
|
7
|
-
"@clinebot/llms": "
|
|
8
|
-
"
|
|
9
|
-
"@clinebot/shared": "workspace:*",
|
|
10
|
-
"nanoid": "^5.1.6",
|
|
6
|
+
"@clinebot/agents": "0.0.2",
|
|
7
|
+
"@clinebot/llms": "0.0.2",
|
|
8
|
+
"nanoid": "^5.1.7",
|
|
11
9
|
"simple-git": "^3.32.3",
|
|
12
10
|
"yaml": "^2.8.2",
|
|
13
11
|
"zod": "^4.3.6"
|
|
14
12
|
},
|
|
15
13
|
"exports": {
|
|
16
14
|
".": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
15
|
+
"browser": "./dist/index.browser.js",
|
|
16
|
+
"development": "./dist/index.node.js",
|
|
17
|
+
"types": "./dist/index.node.d.ts",
|
|
18
|
+
"import": "./dist/index.node.js"
|
|
20
19
|
},
|
|
21
20
|
"./node": {
|
|
22
|
-
"development": "./src/index.ts",
|
|
23
|
-
"types": "./dist/index.d.ts",
|
|
24
|
-
"import": "./dist/index.js"
|
|
21
|
+
"development": "./src/index.node.ts",
|
|
22
|
+
"types": "./dist/index.node.d.ts",
|
|
23
|
+
"import": "./dist/index.node.js"
|
|
25
24
|
},
|
|
26
25
|
"./browser": {
|
|
27
|
-
"development": "./
|
|
26
|
+
"development": "./dist/index.browser.js",
|
|
28
27
|
"types": "./dist/index.browser.d.ts",
|
|
29
28
|
"import": "./dist/index.browser.js"
|
|
30
29
|
},
|
|
31
30
|
"./server": {
|
|
32
|
-
"development": "./
|
|
31
|
+
"development": "./dist/server/index.js",
|
|
33
32
|
"types": "./dist/server/index.d.ts",
|
|
34
33
|
"import": "./dist/server/index.js"
|
|
35
34
|
},
|
|
36
35
|
"./server/node": {
|
|
37
|
-
"development": "./
|
|
36
|
+
"development": "./dist/server/index.js",
|
|
38
37
|
"types": "./dist/server/index.d.ts",
|
|
39
38
|
"import": "./dist/server/index.js"
|
|
40
39
|
},
|
|
41
40
|
"./server/browser": {
|
|
42
|
-
"development": "./
|
|
41
|
+
"development": "./dist/server/index.js",
|
|
43
42
|
"types": "./dist/server.browser.d.ts",
|
|
44
43
|
"import": "./dist/server.browser.js"
|
|
45
44
|
}
|
|
@@ -50,7 +49,7 @@
|
|
|
50
49
|
"src"
|
|
51
50
|
],
|
|
52
51
|
"scripts": {
|
|
53
|
-
"build": "bun run ./
|
|
52
|
+
"build": "bun run ./bun.mts && bun tsc -p tsconfig.build.json",
|
|
54
53
|
"clean": "rm -rf dist node_modules",
|
|
55
54
|
"typecheck": "bun tsc -p tsconfig.dev.json --noEmit",
|
|
56
55
|
"test": "bun run test:unit && bun run test:e2e",
|
|
@@ -59,5 +58,5 @@
|
|
|
59
58
|
"test:watch": "vitest --config vitest.config.ts"
|
|
60
59
|
},
|
|
61
60
|
"type": "module",
|
|
62
|
-
"types": "dist/index.d.ts"
|
|
63
|
-
}
|
|
61
|
+
"types": "dist/index.node.d.ts"
|
|
62
|
+
}
|
|
@@ -226,8 +226,137 @@ describe("zod schema conversion", () => {
|
|
|
226
226
|
description:
|
|
227
227
|
"The absolute file path of a text file to read content from",
|
|
228
228
|
},
|
|
229
|
-
description:
|
|
229
|
+
description:
|
|
230
|
+
"Array of absolute file paths to get full content from. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
|
230
231
|
});
|
|
231
232
|
expect(inputSchema.required).toEqual(["file_paths"]);
|
|
232
233
|
});
|
|
234
|
+
|
|
235
|
+
it("exposes skills args as optional nullable in tool schemas", () => {
|
|
236
|
+
const tools = createDefaultTools({
|
|
237
|
+
executors: {
|
|
238
|
+
skills: async () => "ok",
|
|
239
|
+
},
|
|
240
|
+
enableReadFiles: false,
|
|
241
|
+
enableSearch: false,
|
|
242
|
+
enableBash: false,
|
|
243
|
+
enableWebFetch: false,
|
|
244
|
+
enableEditor: false,
|
|
245
|
+
enableApplyPatch: false,
|
|
246
|
+
enableAskQuestion: false,
|
|
247
|
+
enableSkills: true,
|
|
248
|
+
});
|
|
249
|
+
const skills = tools.find((tool) => tool.name === "skills");
|
|
250
|
+
expect(skills).toBeDefined();
|
|
251
|
+
if (!skills) {
|
|
252
|
+
throw new Error("Expected skills tool.");
|
|
253
|
+
}
|
|
254
|
+
const schema = skills.inputSchema as {
|
|
255
|
+
required?: string[];
|
|
256
|
+
properties?: Record<string, unknown>;
|
|
257
|
+
};
|
|
258
|
+
expect(schema.required).toEqual(["skill"]);
|
|
259
|
+
expect(schema.properties).toHaveProperty("args");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("default editor tool", () => {
|
|
264
|
+
it("accepts null for unused optional fields on str_replace", async () => {
|
|
265
|
+
const execute = vi.fn(async () => "patched");
|
|
266
|
+
const tools = createDefaultTools({
|
|
267
|
+
executors: {
|
|
268
|
+
editor: execute,
|
|
269
|
+
},
|
|
270
|
+
enableReadFiles: false,
|
|
271
|
+
enableSearch: false,
|
|
272
|
+
enableBash: false,
|
|
273
|
+
enableWebFetch: false,
|
|
274
|
+
enableSkills: false,
|
|
275
|
+
enableAskQuestion: false,
|
|
276
|
+
enableApplyPatch: false,
|
|
277
|
+
enableEditor: true,
|
|
278
|
+
});
|
|
279
|
+
const editorTool = tools.find((tool) => tool.name === "editor");
|
|
280
|
+
expect(editorTool).toBeDefined();
|
|
281
|
+
if (!editorTool) {
|
|
282
|
+
throw new Error("Expected editor tool to be defined.");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const result = await editorTool.execute(
|
|
286
|
+
{
|
|
287
|
+
command: "str_replace",
|
|
288
|
+
path: "/tmp/example.ts",
|
|
289
|
+
old_str: "before",
|
|
290
|
+
new_str: "after",
|
|
291
|
+
file_text: null,
|
|
292
|
+
insert_line: null,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
agentId: "agent-1",
|
|
296
|
+
conversationId: "conv-1",
|
|
297
|
+
iteration: 1,
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
expect(result).toEqual({
|
|
302
|
+
query: "str_replace:/tmp/example.ts",
|
|
303
|
+
result: "patched",
|
|
304
|
+
success: true,
|
|
305
|
+
});
|
|
306
|
+
expect(execute).toHaveBeenCalledWith(
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
command: "str_replace",
|
|
309
|
+
path: "/tmp/example.ts",
|
|
310
|
+
old_str: "before",
|
|
311
|
+
new_str: "after",
|
|
312
|
+
file_text: null,
|
|
313
|
+
insert_line: null,
|
|
314
|
+
}),
|
|
315
|
+
process.cwd(),
|
|
316
|
+
expect.objectContaining({
|
|
317
|
+
agentId: "agent-1",
|
|
318
|
+
conversationId: "conv-1",
|
|
319
|
+
iteration: 1,
|
|
320
|
+
}),
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("still rejects null for required insert fields", async () => {
|
|
325
|
+
const execute = vi.fn(async () => "patched");
|
|
326
|
+
const tools = createDefaultTools({
|
|
327
|
+
executors: {
|
|
328
|
+
editor: execute,
|
|
329
|
+
},
|
|
330
|
+
enableReadFiles: false,
|
|
331
|
+
enableSearch: false,
|
|
332
|
+
enableBash: false,
|
|
333
|
+
enableWebFetch: false,
|
|
334
|
+
enableSkills: false,
|
|
335
|
+
enableAskQuestion: false,
|
|
336
|
+
enableApplyPatch: false,
|
|
337
|
+
enableEditor: true,
|
|
338
|
+
});
|
|
339
|
+
const editorTool = tools.find((tool) => tool.name === "editor");
|
|
340
|
+
expect(editorTool).toBeDefined();
|
|
341
|
+
if (!editorTool) {
|
|
342
|
+
throw new Error("Expected editor tool to be defined.");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await expect(
|
|
346
|
+
editorTool.execute(
|
|
347
|
+
{
|
|
348
|
+
command: "insert",
|
|
349
|
+
path: "/tmp/example.ts",
|
|
350
|
+
new_str: "after",
|
|
351
|
+
insert_line: null,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
agentId: "agent-1",
|
|
355
|
+
conversationId: "conv-1",
|
|
356
|
+
iteration: 1,
|
|
357
|
+
},
|
|
358
|
+
),
|
|
359
|
+
).rejects.toThrow(/insert_line is required for command=insert/);
|
|
360
|
+
expect(execute).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
233
362
|
});
|
|
@@ -426,7 +426,7 @@ export function createEditorTool(
|
|
|
426
426
|
name: "editor",
|
|
427
427
|
description:
|
|
428
428
|
"Edit file using absolute path with create, string replacement, and line insert operations. " +
|
|
429
|
-
"Supported commands: create, str_replace, insert
|
|
429
|
+
"Supported commands: create, str_replace, insert.",
|
|
430
430
|
inputSchema: zodToJsonSchema(EditFileInputSchema),
|
|
431
431
|
timeoutMs,
|
|
432
432
|
retryable: false, // Editing operations are stateful and should not auto-retry
|
|
@@ -487,7 +487,11 @@ export function createSkillsTool(
|
|
|
487
487
|
execute: async (input, context) => {
|
|
488
488
|
const validatedInput = validateWithZod(SkillsInputSchema, input);
|
|
489
489
|
return withTimeout(
|
|
490
|
-
executor(
|
|
490
|
+
executor(
|
|
491
|
+
validatedInput.skill,
|
|
492
|
+
validatedInput.args || undefined,
|
|
493
|
+
context,
|
|
494
|
+
),
|
|
491
495
|
timeoutMs,
|
|
492
496
|
`Skills operation timed out after ${timeoutMs}ms`,
|
|
493
497
|
);
|
|
@@ -116,7 +116,7 @@ async function createFile(
|
|
|
116
116
|
async function replaceInFile(
|
|
117
117
|
filePath: string,
|
|
118
118
|
oldStr: string,
|
|
119
|
-
newStr: string | undefined,
|
|
119
|
+
newStr: string | null | undefined,
|
|
120
120
|
encoding: BufferEncoding,
|
|
121
121
|
maxDiffLines: number,
|
|
122
122
|
): Promise<string> {
|
|
@@ -142,23 +142,24 @@ async function replaceInFile(
|
|
|
142
142
|
|
|
143
143
|
async function insertInFile(
|
|
144
144
|
filePath: string,
|
|
145
|
-
|
|
145
|
+
insertLineOneBased: number,
|
|
146
146
|
newStr: string,
|
|
147
147
|
encoding: BufferEncoding,
|
|
148
148
|
): Promise<string> {
|
|
149
149
|
const content = await fs.readFile(filePath, encoding);
|
|
150
150
|
const lines = content.split("\n");
|
|
151
|
+
const insertLine = insertLineOneBased - 1; // Convert to zero-based index
|
|
151
152
|
|
|
152
153
|
if (insertLine < 0 || insertLine > lines.length) {
|
|
153
154
|
throw new Error(
|
|
154
|
-
`Invalid line number: ${
|
|
155
|
+
`Invalid line number: ${insertLineOneBased}. Valid range: 1-${lines.length}`,
|
|
155
156
|
);
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
lines.splice(insertLine, 0, ...newStr.split("\n"));
|
|
159
160
|
await fs.writeFile(filePath, lines.join("\n"), { encoding });
|
|
160
161
|
|
|
161
|
-
return `Inserted content at line ${
|
|
162
|
+
return `Inserted content at line ${insertLineOneBased} in ${filePath}.`;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/**
|
|
@@ -182,7 +183,7 @@ export function createEditorExecutor(
|
|
|
182
183
|
|
|
183
184
|
switch (input.command) {
|
|
184
185
|
case "create":
|
|
185
|
-
if (input.file_text
|
|
186
|
+
if (input.file_text == null) {
|
|
186
187
|
throw new Error(
|
|
187
188
|
"Parameter `file_text` is required for command: create",
|
|
188
189
|
);
|
|
@@ -190,7 +191,7 @@ export function createEditorExecutor(
|
|
|
190
191
|
return createFile(filePath, input.file_text, encoding);
|
|
191
192
|
|
|
192
193
|
case "str_replace":
|
|
193
|
-
if (input.old_str
|
|
194
|
+
if (input.old_str == null) {
|
|
194
195
|
throw new Error(
|
|
195
196
|
"Parameter `old_str` is required for command: str_replace",
|
|
196
197
|
);
|
|
@@ -204,19 +205,19 @@ export function createEditorExecutor(
|
|
|
204
205
|
);
|
|
205
206
|
|
|
206
207
|
case "insert":
|
|
207
|
-
if (input.insert_line
|
|
208
|
+
if (input.insert_line == null) {
|
|
208
209
|
throw new Error(
|
|
209
210
|
"Parameter `insert_line` is required for insert command.",
|
|
210
211
|
);
|
|
211
212
|
}
|
|
212
|
-
if (input.new_str
|
|
213
|
+
if (input.new_str == null) {
|
|
213
214
|
throw new Error(
|
|
214
215
|
"Parameter `new_str` is required for insert command.",
|
|
215
216
|
);
|
|
216
217
|
}
|
|
217
218
|
return insertInFile(
|
|
218
219
|
filePath,
|
|
219
|
-
input.insert_line,
|
|
220
|
+
input.insert_line, // One-based index
|
|
220
221
|
input.new_str,
|
|
221
222
|
encoding,
|
|
222
223
|
);
|
|
@@ -17,7 +17,7 @@ describe("createFileReadExecutor", () => {
|
|
|
17
17
|
conversationId: "conv-1",
|
|
18
18
|
iteration: 1,
|
|
19
19
|
});
|
|
20
|
-
expect(result).toBe("hello absolute path");
|
|
20
|
+
expect(result).toBe("1 | hello absolute path");
|
|
21
21
|
} finally {
|
|
22
22
|
await fs.rm(dir, { recursive: true, force: true });
|
|
23
23
|
}
|
|
@@ -32,6 +32,12 @@ export interface FileReadExecutorOptions {
|
|
|
32
32
|
includeLineNumbers?: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const DEFAULT_FILE_READ_OPTIONS: Required<FileReadExecutorOptions> = {
|
|
36
|
+
maxFileSizeBytes: 10_000_000, // 10MB default limit
|
|
37
|
+
encoding: "utf-8", // Default to UTF-8 encoding
|
|
38
|
+
includeLineNumbers: true, // Include line numbers by default
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* Create a file read executor using Node.js fs module
|
|
37
43
|
*
|
|
@@ -48,11 +54,10 @@ export interface FileReadExecutorOptions {
|
|
|
48
54
|
export function createFileReadExecutor(
|
|
49
55
|
options: FileReadExecutorOptions = {},
|
|
50
56
|
): FileReadExecutor {
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} = options;
|
|
57
|
+
const { maxFileSizeBytes, encoding, includeLineNumbers } = {
|
|
58
|
+
...DEFAULT_FILE_READ_OPTIONS,
|
|
59
|
+
...options,
|
|
60
|
+
};
|
|
56
61
|
|
|
57
62
|
return async (filePath: string, _context: ToolContext): Promise<string> => {
|
|
58
63
|
const resolvedPath = path.isAbsolute(filePath)
|
|
@@ -77,7 +82,7 @@ export function createFileReadExecutor(
|
|
|
77
82
|
// Read file content
|
|
78
83
|
const content = await fs.readFile(resolvedPath, encoding);
|
|
79
84
|
|
|
80
|
-
// Optionally add line numbers
|
|
85
|
+
// Optionally add line numbers - one-based indexing for better readability
|
|
81
86
|
if (includeLineNumbers) {
|
|
82
87
|
const lines = content.split("\n");
|
|
83
88
|
const maxLineNumWidth = String(lines.length).length;
|
|
@@ -37,6 +37,11 @@ export {
|
|
|
37
37
|
type SearchExecutorOptions,
|
|
38
38
|
type WebFetchExecutorOptions,
|
|
39
39
|
} from "./executors/index.js";
|
|
40
|
+
export {
|
|
41
|
+
DEFAULT_MODEL_TOOL_ROUTING_RULES,
|
|
42
|
+
resolveToolRoutingConfig,
|
|
43
|
+
type ToolRoutingRule,
|
|
44
|
+
} from "./model-tool-routing.js";
|
|
40
45
|
// Presets
|
|
41
46
|
export {
|
|
42
47
|
createDefaultToolsWithPreset,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MODEL_TOOL_ROUTING_RULES,
|
|
4
|
+
resolveToolRoutingConfig,
|
|
5
|
+
} from "./model-tool-routing.js";
|
|
6
|
+
|
|
7
|
+
describe("model tool routing", () => {
|
|
8
|
+
it("applies default codex/gpt routing in act mode", () => {
|
|
9
|
+
const config = resolveToolRoutingConfig(
|
|
10
|
+
"openai",
|
|
11
|
+
"openai/gpt-5.4",
|
|
12
|
+
"act",
|
|
13
|
+
DEFAULT_MODEL_TOOL_ROUTING_RULES,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(config.enableApplyPatch).toBe(true);
|
|
17
|
+
expect(config.enableEditor).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("does not apply default codex/gpt routing in plan mode", () => {
|
|
21
|
+
const config = resolveToolRoutingConfig(
|
|
22
|
+
"openai",
|
|
23
|
+
"openai/gpt-5.4",
|
|
24
|
+
"plan",
|
|
25
|
+
DEFAULT_MODEL_TOOL_ROUTING_RULES,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(config).toEqual({});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("applies matching custom rules in order", () => {
|
|
32
|
+
const config = resolveToolRoutingConfig(
|
|
33
|
+
"anthropic",
|
|
34
|
+
"claude-sonnet-4-6",
|
|
35
|
+
"act",
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
name: "claude-editor-off",
|
|
39
|
+
mode: "act",
|
|
40
|
+
modelIdIncludes: ["claude"],
|
|
41
|
+
disableTools: ["editor"],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "claude-apply-patch-on",
|
|
45
|
+
mode: "act",
|
|
46
|
+
modelIdIncludes: ["claude"],
|
|
47
|
+
enableTools: ["apply_patch"],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(config.enableEditor).toBe(false);
|
|
53
|
+
expect(config.enableApplyPatch).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns empty config when no rules match", () => {
|
|
57
|
+
const config = resolveToolRoutingConfig(
|
|
58
|
+
"anthropic",
|
|
59
|
+
"claude-sonnet-4-6",
|
|
60
|
+
"act",
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
mode: "act",
|
|
64
|
+
modelIdIncludes: ["gpt"],
|
|
65
|
+
enableTools: ["apply_patch"],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(config).toEqual({});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("can match provider-only rules", () => {
|
|
74
|
+
const config = resolveToolRoutingConfig("openai", "o4-mini", "act", [
|
|
75
|
+
{
|
|
76
|
+
mode: "act",
|
|
77
|
+
providerIdIncludes: ["openai"],
|
|
78
|
+
enableTools: ["apply_patch"],
|
|
79
|
+
disableTools: ["editor"],
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
expect(config.enableApplyPatch).toBe(true);
|
|
84
|
+
expect(config.enableEditor).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|