klaus-agent 0.2.1 → 0.3.0
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 +69 -15
- package/README.zh-CN.md +69 -15
- package/dist/core/agent-loop.d.ts +4 -1
- package/dist/core/agent-loop.js +20 -3
- package/dist/core/agent-loop.js.map +1 -1
- package/dist/core/agent.d.ts +5 -0
- package/dist/core/agent.js +25 -0
- package/dist/core/agent.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/llm/provider.js +3 -11
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/types.d.ts +17 -0
- package/dist/planning/nag-injection.d.ts +8 -0
- package/dist/planning/nag-injection.js +21 -0
- package/dist/planning/nag-injection.js.map +1 -0
- package/dist/planning/planning-manager.d.ts +27 -0
- package/dist/planning/planning-manager.js +109 -0
- package/dist/planning/planning-manager.js.map +1 -0
- package/dist/planning/tools.d.ts +3 -0
- package/dist/planning/tools.js +50 -0
- package/dist/planning/tools.js.map +1 -0
- package/dist/planning/types.d.ts +30 -0
- package/dist/planning/types.js +6 -0
- package/dist/planning/types.js.map +1 -0
- package/dist/providers/openai-codex.js +1 -71
- package/dist/providers/openai-codex.js.map +1 -1
- package/dist/providers/openai-responses-shared.d.ts +36 -0
- package/dist/providers/openai-responses-shared.js +74 -0
- package/dist/providers/openai-responses-shared.js.map +1 -0
- package/dist/providers/openai-responses.d.ts +7 -0
- package/dist/providers/openai-responses.js +128 -0
- package/dist/providers/openai-responses.js.map +1 -0
- package/dist/providers/openai.js +1 -10
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/shared.d.ts +5 -1
- package/dist/providers/shared.js +20 -0
- package/dist/providers/shared.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/core/agent-loop.ts +25 -3
- package/src/core/agent.ts +30 -0
- package/src/index.ts +20 -3
- package/src/llm/provider.ts +3 -12
- package/src/llm/types.ts +19 -0
- package/src/planning/nag-injection.ts +24 -0
- package/src/planning/planning-manager.ts +133 -0
- package/src/planning/tools.ts +71 -0
- package/src/planning/types.ts +40 -0
- package/src/providers/openai-codex.ts +2 -89
- package/src/providers/openai-responses-shared.ts +97 -0
- package/src/providers/openai-responses.ts +152 -0
- package/src/providers/openai.ts +1 -8
- package/src/providers/shared.ts +19 -1
- package/src/types.ts +4 -0
- package/src/providers/index.ts +0 -7
- package/src/providers/kimi.ts +0 -12
- package/src/providers/minimax.ts +0 -12
- package/src/providers/volcengine.ts +0 -12
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { SubagentConfig } from "./multi-agent/types.js";
|
|
|
15
15
|
import type { SkillSource } from "./skills/types.js";
|
|
16
16
|
import type { MCPServerConfig, MCPClient } from "./tools/mcp-adapter.js";
|
|
17
17
|
import type { TaskFactory } from "./background/types.js";
|
|
18
|
+
import type { PlanningConfig } from "./planning/types.js";
|
|
18
19
|
|
|
19
20
|
export interface CreateAgentConfig {
|
|
20
21
|
// Required
|
|
@@ -41,6 +42,7 @@ export interface CreateAgentConfig {
|
|
|
41
42
|
mcp?: { servers: MCPServerConfig[]; clientFactory: (config: MCPServerConfig) => MCPClient };
|
|
42
43
|
wire?: { bufferSize?: number };
|
|
43
44
|
backgroundTasks?: { factories?: Record<string, TaskFactory> };
|
|
45
|
+
planning?: PlanningConfig;
|
|
44
46
|
|
|
45
47
|
// Advanced: provide your own LLM provider
|
|
46
48
|
provider?: LLMProvider;
|
|
@@ -71,6 +73,7 @@ export function createAgent(config: CreateAgentConfig): Agent {
|
|
|
71
73
|
mcp: config.mcp,
|
|
72
74
|
wire: config.wire,
|
|
73
75
|
backgroundTasks: config.backgroundTasks,
|
|
76
|
+
planning: config.planning,
|
|
74
77
|
});
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -80,10 +83,8 @@ export { ApprovalImpl } from "./approval/approval.js";
|
|
|
80
83
|
export { registerProvider, resolveProvider } from "./llm/provider.js";
|
|
81
84
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
|
82
85
|
export { OpenAIProvider } from "./providers/openai.js";
|
|
86
|
+
export { OpenAIResponsesProvider } from "./providers/openai-responses.js";
|
|
83
87
|
export { GeminiProvider } from "./providers/google.js";
|
|
84
|
-
export { MiniMaxProvider } from "./providers/minimax.js";
|
|
85
|
-
export { KimiProvider } from "./providers/kimi.js";
|
|
86
|
-
export { VolcengineProvider } from "./providers/volcengine.js";
|
|
87
88
|
export { executeToolCalls } from "./tools/executor.js";
|
|
88
89
|
export { SessionManager } from "./session/session-manager.js";
|
|
89
90
|
export { buildSessionContext } from "./session/session-context-builder.js";
|
|
@@ -98,10 +99,14 @@ export { discoverSkills } from "./skills/discovery.js";
|
|
|
98
99
|
export { loadSkill, renderSkillTemplate } from "./skills/loader.js";
|
|
99
100
|
export { MCPAdapter } from "./tools/mcp-adapter.js";
|
|
100
101
|
export { estimateTokens, shouldCompact, findCutPoint } from "./compaction/compaction.js";
|
|
102
|
+
export { calculateCost } from "./providers/shared.js";
|
|
101
103
|
export { LLMSummarizer } from "./compaction/summarizer.js";
|
|
102
104
|
export { Wire } from "./wire/wire.js";
|
|
103
105
|
export { BackgroundTaskManager } from "./background/task-manager.js";
|
|
104
106
|
export { createBackgroundTaskTools } from "./background/tools.js";
|
|
107
|
+
export { PlanningManager } from "./planning/planning-manager.js";
|
|
108
|
+
export { createPlanningTools } from "./planning/tools.js";
|
|
109
|
+
export { PlanningNagProvider } from "./planning/nag-injection.js";
|
|
105
110
|
|
|
106
111
|
// Core types
|
|
107
112
|
export type {
|
|
@@ -144,6 +149,8 @@ export type {
|
|
|
144
149
|
ToolResultMessage,
|
|
145
150
|
Message,
|
|
146
151
|
TokenUsage,
|
|
152
|
+
ModelCost,
|
|
153
|
+
UsageCost,
|
|
147
154
|
ContentBlock,
|
|
148
155
|
TextContent,
|
|
149
156
|
ImageContent,
|
|
@@ -226,3 +233,13 @@ export type {
|
|
|
226
233
|
BackgroundTaskEvent,
|
|
227
234
|
TaskFactory,
|
|
228
235
|
} from "./background/types.js";
|
|
236
|
+
|
|
237
|
+
// Planning types
|
|
238
|
+
export type {
|
|
239
|
+
PlanningConfig,
|
|
240
|
+
PlanPhase,
|
|
241
|
+
TodoItem,
|
|
242
|
+
TodoStatus,
|
|
243
|
+
} from "./planning/types.js";
|
|
244
|
+
|
|
245
|
+
export { PLANNING_TOOL_NAMES } from "./planning/types.js";
|
package/src/llm/provider.ts
CHANGED
|
@@ -3,27 +3,18 @@
|
|
|
3
3
|
import { AnthropicProvider } from "../providers/anthropic.js";
|
|
4
4
|
import { OpenAIProvider } from "../providers/openai.js";
|
|
5
5
|
import { OpenAICodexProvider } from "../providers/openai-codex.js";
|
|
6
|
+
import { OpenAIResponsesProvider } from "../providers/openai-responses.js";
|
|
6
7
|
import { GeminiProvider } from "../providers/google.js";
|
|
7
|
-
import { MiniMaxProvider } from "../providers/minimax.js";
|
|
8
|
-
import { KimiProvider } from "../providers/kimi.js";
|
|
9
|
-
import { VolcengineProvider } from "../providers/volcengine.js";
|
|
10
8
|
import type { LLMProvider, LLMProviderFactory } from "./types.js";
|
|
11
9
|
|
|
12
10
|
const providers = new Map<string, LLMProviderFactory>();
|
|
13
11
|
|
|
14
|
-
// Built-in providers
|
|
12
|
+
// Built-in protocol providers
|
|
15
13
|
providers.set("anthropic", (c) => new AnthropicProvider(c.apiKey, c.baseUrl));
|
|
16
14
|
providers.set("openai", (c) => new OpenAIProvider(c.apiKey, c.baseUrl));
|
|
15
|
+
providers.set("openai-responses", (c) => new OpenAIResponsesProvider(c.apiKey, c.baseUrl));
|
|
17
16
|
providers.set("openai-codex", (c) => new OpenAICodexProvider(c.apiKey, c.baseUrl));
|
|
18
17
|
providers.set("google", (c) => new GeminiProvider(c.apiKey, c.baseUrl));
|
|
19
|
-
providers.set("minimax", (c) => new MiniMaxProvider(c.apiKey, c.baseUrl));
|
|
20
|
-
providers.set("kimi", (c) => new KimiProvider(c.apiKey, c.baseUrl));
|
|
21
|
-
providers.set("volcengine", (c) => new VolcengineProvider(c.apiKey, c.baseUrl));
|
|
22
|
-
|
|
23
|
-
// Protocol-compatible proxies — user provides baseUrl to connect any compatible service
|
|
24
|
-
providers.set("openai-compatible", (c) => new OpenAIProvider(c.apiKey, c.baseUrl));
|
|
25
|
-
providers.set("anthropic-compatible", (c) => new AnthropicProvider(c.apiKey, c.baseUrl));
|
|
26
|
-
providers.set("gemini-compatible", (c) => new GeminiProvider(c.apiKey, c.baseUrl));
|
|
27
18
|
|
|
28
19
|
export function registerProvider(name: string, factory: LLMProviderFactory): void {
|
|
29
20
|
providers.set(name, factory);
|
package/src/llm/types.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
// LLM abstraction types
|
|
2
2
|
|
|
3
|
+
/** Per-token pricing in $/million tokens. */
|
|
4
|
+
export interface ModelCost {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
cacheRead?: number;
|
|
8
|
+
cacheWrite?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Calculated cost in actual dollars for a single request. */
|
|
12
|
+
export interface UsageCost {
|
|
13
|
+
input: number;
|
|
14
|
+
output: number;
|
|
15
|
+
cacheRead: number;
|
|
16
|
+
cacheWrite: number;
|
|
17
|
+
total: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
export interface ModelConfig {
|
|
4
21
|
provider: string;
|
|
5
22
|
model: string;
|
|
@@ -10,6 +27,7 @@ export interface ModelConfig {
|
|
|
10
27
|
vision?: boolean;
|
|
11
28
|
thinking?: boolean;
|
|
12
29
|
};
|
|
30
|
+
cost?: ModelCost;
|
|
13
31
|
}
|
|
14
32
|
|
|
15
33
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
@@ -84,6 +102,7 @@ export interface TokenUsage {
|
|
|
84
102
|
totalTokens: number;
|
|
85
103
|
cacheReadTokens?: number;
|
|
86
104
|
cacheWriteTokens?: number;
|
|
105
|
+
cost?: UsageCost;
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
// --- Streaming events ---
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Nag injection provider — reminds model to update todos when it hasn't for N rounds
|
|
2
|
+
|
|
3
|
+
import type { DynamicInjectionProvider, DynamicInjection } from "../injection/types.js";
|
|
4
|
+
import type { AgentMessage } from "../types.js";
|
|
5
|
+
import type { PlanningManager } from "./planning-manager.js";
|
|
6
|
+
|
|
7
|
+
export class PlanningNagProvider implements DynamicInjectionProvider {
|
|
8
|
+
constructor(private _manager: PlanningManager) {}
|
|
9
|
+
|
|
10
|
+
async getInjections(_history: AgentMessage[]): Promise<DynamicInjection[]> {
|
|
11
|
+
if (!this._manager.shouldNag()) return [];
|
|
12
|
+
|
|
13
|
+
// Reset after check so the next nag waits another N rounds.
|
|
14
|
+
// Kept here (not in shouldNag) so shouldNag() stays side-effect-free.
|
|
15
|
+
this._manager.resetRoundCounter();
|
|
16
|
+
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
type: "planning-nag",
|
|
20
|
+
content: this._manager.getNagMessage(),
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Planning manager — two-phase planning with structured todo tracking
|
|
2
|
+
|
|
3
|
+
import type { TodoItem, TodoStatus, PlanPhase, PlanningState, PlanningConfig } from "./types.js";
|
|
4
|
+
import { PLANNING_TOOL_NAMES } from "./types.js";
|
|
5
|
+
import { generateId } from "../utils/id.js";
|
|
6
|
+
|
|
7
|
+
export class PlanningManager {
|
|
8
|
+
private _state: PlanningState;
|
|
9
|
+
private _config: PlanningConfig;
|
|
10
|
+
private _allowedInPlanning: ReadonlySet<string>;
|
|
11
|
+
|
|
12
|
+
constructor(config: PlanningConfig = {}) {
|
|
13
|
+
this._config = config;
|
|
14
|
+
const allowed = new Set(config.readOnlyTools ?? []);
|
|
15
|
+
allowed.add(PLANNING_TOOL_NAMES.todo);
|
|
16
|
+
allowed.add(PLANNING_TOOL_NAMES.planMode);
|
|
17
|
+
this._allowedInPlanning = allowed;
|
|
18
|
+
this._state = {
|
|
19
|
+
phase: "planning",
|
|
20
|
+
todos: [],
|
|
21
|
+
roundsSinceTodoUpdate: 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get phase(): PlanPhase {
|
|
26
|
+
return this._state.phase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get todos(): readonly Readonly<TodoItem>[] {
|
|
30
|
+
return this._state.todos;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get roundsSinceTodoUpdate(): number {
|
|
34
|
+
return this._state.roundsSinceTodoUpdate;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get config(): Readonly<PlanningConfig> {
|
|
38
|
+
return this._config;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Pre-built set of tool names allowed during planning phase. */
|
|
42
|
+
get allowedInPlanning(): ReadonlySet<string> {
|
|
43
|
+
return this._allowedInPlanning;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- Phase control ---
|
|
47
|
+
|
|
48
|
+
startExecution(): string {
|
|
49
|
+
if (this._state.todos.length === 0) {
|
|
50
|
+
throw new Error("Cannot start execution: no todos defined. Create a plan first.");
|
|
51
|
+
}
|
|
52
|
+
this._state.phase = "executing";
|
|
53
|
+
this.resetRoundCounter();
|
|
54
|
+
return `Switched to execution phase. ${this._state.todos.length} todo(s) to complete.\n\n${this.render()}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switchToPlanning(): string {
|
|
58
|
+
this._state.phase = "planning";
|
|
59
|
+
this.resetRoundCounter();
|
|
60
|
+
return `Switched to planning phase. Tools restricted to read-only.\n\n${this.render()}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Todo CRUD ---
|
|
64
|
+
|
|
65
|
+
updateTodos(items: Array<{ id: string; text: string; status: TodoStatus }>): string {
|
|
66
|
+
const max = this._config.maxTodos ?? 50;
|
|
67
|
+
if (items.length > max) {
|
|
68
|
+
throw new Error(`Too many todos: ${items.length} exceeds limit of ${max}.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let inProgressCount = 0;
|
|
72
|
+
const validated: TodoItem[] = [];
|
|
73
|
+
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
const status = item.status ?? "pending";
|
|
76
|
+
if (status === "in_progress") inProgressCount++;
|
|
77
|
+
validated.push({
|
|
78
|
+
id: item.id || generateId(),
|
|
79
|
+
text: item.text,
|
|
80
|
+
status,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (inProgressCount > 1) {
|
|
85
|
+
throw new Error("Only one todo can be in_progress at a time.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._state.todos = validated;
|
|
89
|
+
this.resetRoundCounter();
|
|
90
|
+
return this.render();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Render ---
|
|
94
|
+
|
|
95
|
+
render(): string {
|
|
96
|
+
if (this._state.todos.length === 0) {
|
|
97
|
+
return `[phase: ${this._state.phase}] No todos.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const lines = this._state.todos.map((t) => {
|
|
101
|
+
const icon = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[>]" : "[ ]";
|
|
102
|
+
return `${icon} ${t.id}: ${t.text}`;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const done = this._state.todos.filter((t) => t.status === "completed").length;
|
|
106
|
+
const total = this._state.todos.length;
|
|
107
|
+
|
|
108
|
+
return `[phase: ${this._state.phase}] Progress: ${done}/${total}\n${lines.join("\n")}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- Nag tracking ---
|
|
112
|
+
|
|
113
|
+
/** Call once per agent loop step (after tool execution). */
|
|
114
|
+
tickRound(): void {
|
|
115
|
+
this._state.roundsSinceTodoUpdate++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Reset the round counter (called when the model updates todos). */
|
|
119
|
+
resetRoundCounter(): void {
|
|
120
|
+
this._state.roundsSinceTodoUpdate = 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
shouldNag(): boolean {
|
|
124
|
+
if (this._state.phase !== "executing") return false;
|
|
125
|
+
if (this._state.todos.length === 0) return false;
|
|
126
|
+
const threshold = this._config.nagAfterRounds ?? 3;
|
|
127
|
+
return this._state.roundsSinceTodoUpdate >= threshold;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getNagMessage(): string {
|
|
131
|
+
return this._config.nagMessage ?? "<reminder>Update your todos to reflect current progress.</reminder>";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Planning tools — todo management + phase switching
|
|
2
|
+
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import type { AgentTool, AgentToolResult } from "../tools/types.js";
|
|
5
|
+
import type { PlanningManager } from "./planning-manager.js";
|
|
6
|
+
import { PLANNING_TOOL_NAMES } from "./types.js";
|
|
7
|
+
import type { TodoStatus } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export function createPlanningTools(manager: PlanningManager): AgentTool[] {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
name: PLANNING_TOOL_NAMES.todo,
|
|
13
|
+
label: "Todo",
|
|
14
|
+
description:
|
|
15
|
+
"Manage your task list. Use this tool to plan work, track progress, and stay on track. " +
|
|
16
|
+
"Only one todo can be in_progress at a time. Update todos frequently to reflect your current state.",
|
|
17
|
+
parameters: Type.Object({
|
|
18
|
+
items: Type.Array(
|
|
19
|
+
Type.Object({
|
|
20
|
+
id: Type.String({ description: "Unique ID for the todo item." }),
|
|
21
|
+
text: Type.String({ description: "Description of the task." }),
|
|
22
|
+
status: Type.Union(
|
|
23
|
+
[Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")],
|
|
24
|
+
{ description: "Task status. Only one item can be in_progress at a time." },
|
|
25
|
+
),
|
|
26
|
+
}),
|
|
27
|
+
{ description: "The full updated todo list (replaces previous list)." },
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
async execute(
|
|
31
|
+
_toolCallId: string,
|
|
32
|
+
params: { items: Array<{ id: string; text: string; status: TodoStatus }> },
|
|
33
|
+
): Promise<AgentToolResult> {
|
|
34
|
+
const result = manager.updateTodos(params.items);
|
|
35
|
+
return { content: [{ type: "text", text: result }] };
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: PLANNING_TOOL_NAMES.planMode,
|
|
40
|
+
label: "Plan Mode",
|
|
41
|
+
description:
|
|
42
|
+
"Switch between planning and execution phases. " +
|
|
43
|
+
"In planning phase, only read-only tools are available — use this time to analyze and create todos. " +
|
|
44
|
+
"In execution phase, all tools are available and nag reminders will prompt you to update todos.",
|
|
45
|
+
parameters: Type.Object({
|
|
46
|
+
action: Type.Union(
|
|
47
|
+
[Type.Literal("start_execution"), Type.Literal("switch_to_planning"), Type.Literal("status")],
|
|
48
|
+
{ description: "Action to perform." },
|
|
49
|
+
),
|
|
50
|
+
}),
|
|
51
|
+
async execute(
|
|
52
|
+
_toolCallId: string,
|
|
53
|
+
params: { action: "start_execution" | "switch_to_planning" | "status" },
|
|
54
|
+
): Promise<AgentToolResult> {
|
|
55
|
+
let result: string;
|
|
56
|
+
switch (params.action) {
|
|
57
|
+
case "start_execution":
|
|
58
|
+
result = manager.startExecution();
|
|
59
|
+
break;
|
|
60
|
+
case "switch_to_planning":
|
|
61
|
+
result = manager.switchToPlanning();
|
|
62
|
+
break;
|
|
63
|
+
case "status":
|
|
64
|
+
result = manager.render();
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
return { content: [{ type: "text", text: result }] };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Planning module types — two-phase planning + structured todo tracking
|
|
2
|
+
|
|
3
|
+
export const PLANNING_TOOL_NAMES = {
|
|
4
|
+
todo: "todo",
|
|
5
|
+
planMode: "plan_mode",
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
9
|
+
|
|
10
|
+
export interface TodoItem {
|
|
11
|
+
id: string;
|
|
12
|
+
text: string;
|
|
13
|
+
status: TodoStatus;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PlanPhase = "planning" | "executing";
|
|
17
|
+
|
|
18
|
+
export interface PlanningState {
|
|
19
|
+
phase: PlanPhase;
|
|
20
|
+
todos: TodoItem[];
|
|
21
|
+
roundsSinceTodoUpdate: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PlanningConfig {
|
|
25
|
+
/**
|
|
26
|
+
* Tool names allowed during the planning phase (read-only tools).
|
|
27
|
+
* If omitted or empty, all tools are available during planning
|
|
28
|
+
* (phase separation is advisory only via system prompt).
|
|
29
|
+
*/
|
|
30
|
+
readOnlyTools?: string[];
|
|
31
|
+
|
|
32
|
+
/** Number of rounds without a todo update before injecting a nag reminder. Default: 3. */
|
|
33
|
+
nagAfterRounds?: number;
|
|
34
|
+
|
|
35
|
+
/** Custom nag reminder text. */
|
|
36
|
+
nagMessage?: string;
|
|
37
|
+
|
|
38
|
+
/** Maximum number of todo items. Default: 50. */
|
|
39
|
+
maxTodos?: number;
|
|
40
|
+
}
|
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
ToolDefinition,
|
|
15
15
|
} from "../llm/types.js";
|
|
16
16
|
import { platform, release, arch } from "node:os";
|
|
17
|
+
import { mapMessages, mapTools } from "./openai-responses-shared.js";
|
|
18
|
+
import type { ResponseInput, ResponseTool } from "./openai-responses-shared.js";
|
|
17
19
|
|
|
18
20
|
// --- Configuration ---
|
|
19
21
|
|
|
@@ -41,26 +43,6 @@ interface CodexRequestBody {
|
|
|
41
43
|
[key: string]: unknown;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
type ResponseInput = ResponseInputItem[];
|
|
45
|
-
|
|
46
|
-
type ResponseInputItem =
|
|
47
|
-
| { type: "message"; role: "user" | "assistant"; content: ResponseContent[] }
|
|
48
|
-
| { type: "function_call"; id: string; call_id: string; name: string; arguments: string }
|
|
49
|
-
| { type: "function_call_output"; call_id: string; output: string };
|
|
50
|
-
|
|
51
|
-
type ResponseContent =
|
|
52
|
-
| { type: "input_text"; text: string }
|
|
53
|
-
| { type: "output_text"; text: string }
|
|
54
|
-
| { type: "input_image"; image_url: string };
|
|
55
|
-
|
|
56
|
-
interface ResponseTool {
|
|
57
|
-
type: "function";
|
|
58
|
-
name: string;
|
|
59
|
-
description: string;
|
|
60
|
-
parameters: Record<string, unknown>;
|
|
61
|
-
strict: null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
46
|
// --- Provider ---
|
|
65
47
|
|
|
66
48
|
export class OpenAICodexProvider implements LLMProvider {
|
|
@@ -289,75 +271,6 @@ function buildRequestBody(
|
|
|
289
271
|
return body;
|
|
290
272
|
}
|
|
291
273
|
|
|
292
|
-
function mapMessages(messages: Message[]): ResponseInput {
|
|
293
|
-
const input: ResponseInput = [];
|
|
294
|
-
|
|
295
|
-
for (const m of messages) {
|
|
296
|
-
if (m.role === "user") {
|
|
297
|
-
const content: ResponseContent[] = [];
|
|
298
|
-
if (typeof m.content === "string") {
|
|
299
|
-
content.push({ type: "input_text", text: m.content });
|
|
300
|
-
} else {
|
|
301
|
-
for (const block of m.content) {
|
|
302
|
-
if (block.type === "text") {
|
|
303
|
-
content.push({ type: "input_text", text: block.text });
|
|
304
|
-
} else if (block.type === "image") {
|
|
305
|
-
const url = block.source.type === "url"
|
|
306
|
-
? block.source.url
|
|
307
|
-
: `data:${block.source.mediaType};base64,${block.source.data}`;
|
|
308
|
-
content.push({ type: "input_image", image_url: url });
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
input.push({ type: "message", role: "user", content });
|
|
313
|
-
} else if (m.role === "assistant") {
|
|
314
|
-
const content: ResponseContent[] = [];
|
|
315
|
-
for (const block of m.content) {
|
|
316
|
-
if (block.type === "text") {
|
|
317
|
-
content.push({ type: "output_text", text: block.text });
|
|
318
|
-
} else if (block.type === "tool_call") {
|
|
319
|
-
// Flush accumulated text before the tool call
|
|
320
|
-
if (content.length > 0) {
|
|
321
|
-
input.push({ type: "message", role: "assistant", content: [...content] });
|
|
322
|
-
content.length = 0;
|
|
323
|
-
}
|
|
324
|
-
input.push({
|
|
325
|
-
type: "function_call",
|
|
326
|
-
id: block.id,
|
|
327
|
-
call_id: block.id,
|
|
328
|
-
name: block.name,
|
|
329
|
-
arguments: JSON.stringify(block.input),
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
// Remaining text content
|
|
334
|
-
if (content.length > 0) {
|
|
335
|
-
input.push({ type: "message", role: "assistant", content });
|
|
336
|
-
}
|
|
337
|
-
} else if (m.role === "tool_result") {
|
|
338
|
-
const output = typeof m.content === "string"
|
|
339
|
-
? m.content
|
|
340
|
-
: m.content.map((b) => b.type === "text" ? b.text : JSON.stringify(b)).join("\n");
|
|
341
|
-
input.push({
|
|
342
|
-
type: "function_call_output",
|
|
343
|
-
call_id: m.toolCallId,
|
|
344
|
-
output,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return input;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function mapTools(tools: ToolDefinition[]): ResponseTool[] {
|
|
353
|
-
return tools.map((t) => ({
|
|
354
|
-
type: "function" as const,
|
|
355
|
-
name: t.name,
|
|
356
|
-
description: t.description,
|
|
357
|
-
parameters: t.inputSchema,
|
|
358
|
-
strict: null,
|
|
359
|
-
}));
|
|
360
|
-
}
|
|
361
274
|
|
|
362
275
|
function mapReasoningEffort(modelId: string, level?: ThinkingLevel): string | undefined {
|
|
363
276
|
if (!level || level === "off") return undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Shared types and utilities for OpenAI Responses API providers (openai-responses, openai-codex)
|
|
2
|
+
|
|
3
|
+
import type { Message, ToolDefinition } from "../llm/types.js";
|
|
4
|
+
|
|
5
|
+
// --- Types ---
|
|
6
|
+
|
|
7
|
+
export type ResponseInput = ResponseInputItem[];
|
|
8
|
+
|
|
9
|
+
export type ResponseInputItem =
|
|
10
|
+
| { type: "message"; role: "user" | "assistant"; content: ResponseContent[] }
|
|
11
|
+
| { type: "function_call"; id: string; call_id: string; name: string; arguments: string }
|
|
12
|
+
| { type: "function_call_output"; call_id: string; output: string };
|
|
13
|
+
|
|
14
|
+
export type ResponseContent =
|
|
15
|
+
| { type: "input_text"; text: string }
|
|
16
|
+
| { type: "output_text"; text: string }
|
|
17
|
+
| { type: "input_image"; image_url: string };
|
|
18
|
+
|
|
19
|
+
export interface ResponseTool {
|
|
20
|
+
type: "function";
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
parameters: Record<string, unknown>;
|
|
24
|
+
strict: null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Message conversion ---
|
|
28
|
+
|
|
29
|
+
export function mapMessages(messages: Message[]): ResponseInput {
|
|
30
|
+
const input: ResponseInput = [];
|
|
31
|
+
|
|
32
|
+
for (const m of messages) {
|
|
33
|
+
if (m.role === "user") {
|
|
34
|
+
const content: ResponseContent[] = [];
|
|
35
|
+
if (typeof m.content === "string") {
|
|
36
|
+
content.push({ type: "input_text", text: m.content });
|
|
37
|
+
} else {
|
|
38
|
+
for (const block of m.content) {
|
|
39
|
+
if (block.type === "text") {
|
|
40
|
+
content.push({ type: "input_text", text: block.text });
|
|
41
|
+
} else if (block.type === "image") {
|
|
42
|
+
const url = block.source.type === "url"
|
|
43
|
+
? block.source.url
|
|
44
|
+
: `data:${block.source.mediaType};base64,${block.source.data}`;
|
|
45
|
+
content.push({ type: "input_image", image_url: url });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
input.push({ type: "message", role: "user", content });
|
|
50
|
+
} else if (m.role === "assistant") {
|
|
51
|
+
const content: ResponseContent[] = [];
|
|
52
|
+
for (const block of m.content) {
|
|
53
|
+
if (block.type === "text") {
|
|
54
|
+
content.push({ type: "output_text", text: block.text });
|
|
55
|
+
} else if (block.type === "tool_call") {
|
|
56
|
+
// Flush accumulated text before the tool call
|
|
57
|
+
if (content.length > 0) {
|
|
58
|
+
input.push({ type: "message", role: "assistant", content: [...content] });
|
|
59
|
+
content.length = 0;
|
|
60
|
+
}
|
|
61
|
+
input.push({
|
|
62
|
+
type: "function_call",
|
|
63
|
+
id: block.id,
|
|
64
|
+
call_id: block.id,
|
|
65
|
+
name: block.name,
|
|
66
|
+
arguments: JSON.stringify(block.input),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Remaining text content
|
|
71
|
+
if (content.length > 0) {
|
|
72
|
+
input.push({ type: "message", role: "assistant", content });
|
|
73
|
+
}
|
|
74
|
+
} else if (m.role === "tool_result") {
|
|
75
|
+
const output = typeof m.content === "string"
|
|
76
|
+
? m.content
|
|
77
|
+
: m.content.map((b) => b.type === "text" ? b.text : JSON.stringify(b)).join("\n");
|
|
78
|
+
input.push({
|
|
79
|
+
type: "function_call_output",
|
|
80
|
+
call_id: m.toolCallId,
|
|
81
|
+
output,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return input;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function mapTools(tools: ToolDefinition[]): ResponseTool[] {
|
|
90
|
+
return tools.map((t) => ({
|
|
91
|
+
type: "function" as const,
|
|
92
|
+
name: t.name,
|
|
93
|
+
description: t.description,
|
|
94
|
+
parameters: t.inputSchema,
|
|
95
|
+
strict: null,
|
|
96
|
+
}));
|
|
97
|
+
}
|