klaus-agent 0.2.2 → 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 +44 -8
- package/README.zh-CN.md +44 -8
- package/dist/core/agent-loop.d.ts +2 -0
- package/dist/core/agent-loop.js +15 -2
- package/dist/core/agent-loop.js.map +1 -1
- package/dist/core/agent.d.ts +5 -0
- package/dist/core/agent.js +24 -0
- package/dist/core/agent.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- 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/package.json +1 -1
- package/src/core/agent-loop.ts +19 -2
- package/src/core/agent.ts +29 -0
- package/src/index.ts +16 -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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Nag injection provider — reminds model to update todos when it hasn't for N rounds
|
|
2
|
+
export class PlanningNagProvider {
|
|
3
|
+
_manager;
|
|
4
|
+
constructor(_manager) {
|
|
5
|
+
this._manager = _manager;
|
|
6
|
+
}
|
|
7
|
+
async getInjections(_history) {
|
|
8
|
+
if (!this._manager.shouldNag())
|
|
9
|
+
return [];
|
|
10
|
+
// Reset after check so the next nag waits another N rounds.
|
|
11
|
+
// Kept here (not in shouldNag) so shouldNag() stays side-effect-free.
|
|
12
|
+
this._manager.resetRoundCounter();
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
type: "planning-nag",
|
|
16
|
+
content: this._manager.getNagMessage(),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=nag-injection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nag-injection.js","sourceRoot":"","sources":["../../src/planning/nag-injection.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAMrF,MAAM,OAAO,mBAAmB;IACV;IAApB,YAAoB,QAAyB;QAAzB,aAAQ,GAAR,QAAQ,CAAiB;IAAG,CAAC;IAEjD,KAAK,CAAC,aAAa,CAAC,QAAwB;QAC1C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE;YAAE,OAAO,EAAE,CAAC;QAE1C,4DAA4D;QAC5D,sEAAsE;QACtE,IAAI,CAAC,QAAQ,CAAC,iBAAiB,EAAE,CAAC;QAElC,OAAO;YACL;gBACE,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE;aACvC;SACF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TodoItem, TodoStatus, PlanPhase, PlanningConfig } from "./types.js";
|
|
2
|
+
export declare class PlanningManager {
|
|
3
|
+
private _state;
|
|
4
|
+
private _config;
|
|
5
|
+
private _allowedInPlanning;
|
|
6
|
+
constructor(config?: PlanningConfig);
|
|
7
|
+
get phase(): PlanPhase;
|
|
8
|
+
get todos(): readonly Readonly<TodoItem>[];
|
|
9
|
+
get roundsSinceTodoUpdate(): number;
|
|
10
|
+
get config(): Readonly<PlanningConfig>;
|
|
11
|
+
/** Pre-built set of tool names allowed during planning phase. */
|
|
12
|
+
get allowedInPlanning(): ReadonlySet<string>;
|
|
13
|
+
startExecution(): string;
|
|
14
|
+
switchToPlanning(): string;
|
|
15
|
+
updateTodos(items: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
text: string;
|
|
18
|
+
status: TodoStatus;
|
|
19
|
+
}>): string;
|
|
20
|
+
render(): string;
|
|
21
|
+
/** Call once per agent loop step (after tool execution). */
|
|
22
|
+
tickRound(): void;
|
|
23
|
+
/** Reset the round counter (called when the model updates todos). */
|
|
24
|
+
resetRoundCounter(): void;
|
|
25
|
+
shouldNag(): boolean;
|
|
26
|
+
getNagMessage(): string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Planning manager — two-phase planning with structured todo tracking
|
|
2
|
+
import { PLANNING_TOOL_NAMES } from "./types.js";
|
|
3
|
+
import { generateId } from "../utils/id.js";
|
|
4
|
+
export class PlanningManager {
|
|
5
|
+
_state;
|
|
6
|
+
_config;
|
|
7
|
+
_allowedInPlanning;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this._config = config;
|
|
10
|
+
const allowed = new Set(config.readOnlyTools ?? []);
|
|
11
|
+
allowed.add(PLANNING_TOOL_NAMES.todo);
|
|
12
|
+
allowed.add(PLANNING_TOOL_NAMES.planMode);
|
|
13
|
+
this._allowedInPlanning = allowed;
|
|
14
|
+
this._state = {
|
|
15
|
+
phase: "planning",
|
|
16
|
+
todos: [],
|
|
17
|
+
roundsSinceTodoUpdate: 0,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
get phase() {
|
|
21
|
+
return this._state.phase;
|
|
22
|
+
}
|
|
23
|
+
get todos() {
|
|
24
|
+
return this._state.todos;
|
|
25
|
+
}
|
|
26
|
+
get roundsSinceTodoUpdate() {
|
|
27
|
+
return this._state.roundsSinceTodoUpdate;
|
|
28
|
+
}
|
|
29
|
+
get config() {
|
|
30
|
+
return this._config;
|
|
31
|
+
}
|
|
32
|
+
/** Pre-built set of tool names allowed during planning phase. */
|
|
33
|
+
get allowedInPlanning() {
|
|
34
|
+
return this._allowedInPlanning;
|
|
35
|
+
}
|
|
36
|
+
// --- Phase control ---
|
|
37
|
+
startExecution() {
|
|
38
|
+
if (this._state.todos.length === 0) {
|
|
39
|
+
throw new Error("Cannot start execution: no todos defined. Create a plan first.");
|
|
40
|
+
}
|
|
41
|
+
this._state.phase = "executing";
|
|
42
|
+
this.resetRoundCounter();
|
|
43
|
+
return `Switched to execution phase. ${this._state.todos.length} todo(s) to complete.\n\n${this.render()}`;
|
|
44
|
+
}
|
|
45
|
+
switchToPlanning() {
|
|
46
|
+
this._state.phase = "planning";
|
|
47
|
+
this.resetRoundCounter();
|
|
48
|
+
return `Switched to planning phase. Tools restricted to read-only.\n\n${this.render()}`;
|
|
49
|
+
}
|
|
50
|
+
// --- Todo CRUD ---
|
|
51
|
+
updateTodos(items) {
|
|
52
|
+
const max = this._config.maxTodos ?? 50;
|
|
53
|
+
if (items.length > max) {
|
|
54
|
+
throw new Error(`Too many todos: ${items.length} exceeds limit of ${max}.`);
|
|
55
|
+
}
|
|
56
|
+
let inProgressCount = 0;
|
|
57
|
+
const validated = [];
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
const status = item.status ?? "pending";
|
|
60
|
+
if (status === "in_progress")
|
|
61
|
+
inProgressCount++;
|
|
62
|
+
validated.push({
|
|
63
|
+
id: item.id || generateId(),
|
|
64
|
+
text: item.text,
|
|
65
|
+
status,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (inProgressCount > 1) {
|
|
69
|
+
throw new Error("Only one todo can be in_progress at a time.");
|
|
70
|
+
}
|
|
71
|
+
this._state.todos = validated;
|
|
72
|
+
this.resetRoundCounter();
|
|
73
|
+
return this.render();
|
|
74
|
+
}
|
|
75
|
+
// --- Render ---
|
|
76
|
+
render() {
|
|
77
|
+
if (this._state.todos.length === 0) {
|
|
78
|
+
return `[phase: ${this._state.phase}] No todos.`;
|
|
79
|
+
}
|
|
80
|
+
const lines = this._state.todos.map((t) => {
|
|
81
|
+
const icon = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[>]" : "[ ]";
|
|
82
|
+
return `${icon} ${t.id}: ${t.text}`;
|
|
83
|
+
});
|
|
84
|
+
const done = this._state.todos.filter((t) => t.status === "completed").length;
|
|
85
|
+
const total = this._state.todos.length;
|
|
86
|
+
return `[phase: ${this._state.phase}] Progress: ${done}/${total}\n${lines.join("\n")}`;
|
|
87
|
+
}
|
|
88
|
+
// --- Nag tracking ---
|
|
89
|
+
/** Call once per agent loop step (after tool execution). */
|
|
90
|
+
tickRound() {
|
|
91
|
+
this._state.roundsSinceTodoUpdate++;
|
|
92
|
+
}
|
|
93
|
+
/** Reset the round counter (called when the model updates todos). */
|
|
94
|
+
resetRoundCounter() {
|
|
95
|
+
this._state.roundsSinceTodoUpdate = 0;
|
|
96
|
+
}
|
|
97
|
+
shouldNag() {
|
|
98
|
+
if (this._state.phase !== "executing")
|
|
99
|
+
return false;
|
|
100
|
+
if (this._state.todos.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
const threshold = this._config.nagAfterRounds ?? 3;
|
|
103
|
+
return this._state.roundsSinceTodoUpdate >= threshold;
|
|
104
|
+
}
|
|
105
|
+
getNagMessage() {
|
|
106
|
+
return this._config.nagMessage ?? "<reminder>Update your todos to reflect current progress.</reminder>";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=planning-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planning-manager.js","sourceRoot":"","sources":["../../src/planning/planning-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Planning tools — todo management + phase switching
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { PLANNING_TOOL_NAMES } from "./types.js";
|
|
4
|
+
export function createPlanningTools(manager) {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
name: PLANNING_TOOL_NAMES.todo,
|
|
8
|
+
label: "Todo",
|
|
9
|
+
description: "Manage your task list. Use this tool to plan work, track progress, and stay on track. " +
|
|
10
|
+
"Only one todo can be in_progress at a time. Update todos frequently to reflect your current state.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
items: Type.Array(Type.Object({
|
|
13
|
+
id: Type.String({ description: "Unique ID for the todo item." }),
|
|
14
|
+
text: Type.String({ description: "Description of the task." }),
|
|
15
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")], { description: "Task status. Only one item can be in_progress at a time." }),
|
|
16
|
+
}), { description: "The full updated todo list (replaces previous list)." }),
|
|
17
|
+
}),
|
|
18
|
+
async execute(_toolCallId, params) {
|
|
19
|
+
const result = manager.updateTodos(params.items);
|
|
20
|
+
return { content: [{ type: "text", text: result }] };
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: PLANNING_TOOL_NAMES.planMode,
|
|
25
|
+
label: "Plan Mode",
|
|
26
|
+
description: "Switch between planning and execution phases. " +
|
|
27
|
+
"In planning phase, only read-only tools are available — use this time to analyze and create todos. " +
|
|
28
|
+
"In execution phase, all tools are available and nag reminders will prompt you to update todos.",
|
|
29
|
+
parameters: Type.Object({
|
|
30
|
+
action: Type.Union([Type.Literal("start_execution"), Type.Literal("switch_to_planning"), Type.Literal("status")], { description: "Action to perform." }),
|
|
31
|
+
}),
|
|
32
|
+
async execute(_toolCallId, params) {
|
|
33
|
+
let result;
|
|
34
|
+
switch (params.action) {
|
|
35
|
+
case "start_execution":
|
|
36
|
+
result = manager.startExecution();
|
|
37
|
+
break;
|
|
38
|
+
case "switch_to_planning":
|
|
39
|
+
result = manager.switchToPlanning();
|
|
40
|
+
break;
|
|
41
|
+
case "status":
|
|
42
|
+
result = manager.render();
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
return { content: [{ type: "text", text: result }] };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/planning/tools.ts"],"names":[],"mappings":"AAAA,qDAAqD;AAErD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAGzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,UAAU,mBAAmB,CAAC,OAAwB;IAC1D,OAAO;QACL;YACE,IAAI,EAAE,mBAAmB,CAAC,IAAI;YAC9B,KAAK,EAAE,MAAM;YACb,WAAW,EACT,wFAAwF;gBACxF,oGAAoG;YACtG,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC;gBACtB,KAAK,EAAE,IAAI,CAAC,KAAK,CACf,IAAI,CAAC,MAAM,CAAC;oBACV,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;oBAChE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,0BAA0B,EAAE,CAAC;oBAC9D,MAAM,EAAE,IAAI,CAAC,KAAK,CAChB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,EACjF,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAC5E;iBACF,CAAC,EACF,EAAE,WAAW,EAAE,sDAAsD,EAAE,CACxE;aACF,CAAC;YACF,KAAK,CAAC,OAAO,CACX,WAAmB,EACnB,MAA0E;gBAE1E,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACjD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;YACvD,CAAC;SACF;QACD;YACE,IAAI,EAAE,mBAAmB,CAAC,QAAQ;YAClC,KAAK,EAAE,WAAW;YAClB,WAAW,EACT,gDAAgD;gBAChD,qGAAqG;gBACrG,gGAAgG;YAClG,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC;gBACtB,MAAM,EAAE,IAAI,CAAC,KAAK,CAChB,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAC7F,EAAE,WAAW,EAAE,oBAAoB,EAAE,CACtC;aACF,CAAC;YACF,KAAK,CAAC,OAAO,CACX,WAAmB,EACnB,MAAuE;gBAEvE,IAAI,MAAc,CAAC;gBACnB,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;oBACtB,KAAK,iBAAiB;wBACpB,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;wBAClC,MAAM;oBACR,KAAK,oBAAoB;wBACvB,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;wBACpC,MAAM;oBACR,KAAK,QAAQ;wBACX,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;wBAC1B,MAAM;gBACV,CAAC;gBACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;YACvD,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const PLANNING_TOOL_NAMES: {
|
|
2
|
+
readonly todo: "todo";
|
|
3
|
+
readonly planMode: "plan_mode";
|
|
4
|
+
};
|
|
5
|
+
export type TodoStatus = "pending" | "in_progress" | "completed";
|
|
6
|
+
export interface TodoItem {
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
status: TodoStatus;
|
|
10
|
+
}
|
|
11
|
+
export type PlanPhase = "planning" | "executing";
|
|
12
|
+
export interface PlanningState {
|
|
13
|
+
phase: PlanPhase;
|
|
14
|
+
todos: TodoItem[];
|
|
15
|
+
roundsSinceTodoUpdate: number;
|
|
16
|
+
}
|
|
17
|
+
export interface PlanningConfig {
|
|
18
|
+
/**
|
|
19
|
+
* Tool names allowed during the planning phase (read-only tools).
|
|
20
|
+
* If omitted or empty, all tools are available during planning
|
|
21
|
+
* (phase separation is advisory only via system prompt).
|
|
22
|
+
*/
|
|
23
|
+
readOnlyTools?: string[];
|
|
24
|
+
/** Number of rounds without a todo update before injecting a nag reminder. Default: 3. */
|
|
25
|
+
nagAfterRounds?: number;
|
|
26
|
+
/** Custom nag reminder text. */
|
|
27
|
+
nagMessage?: string;
|
|
28
|
+
/** Maximum number of todo items. Default: 50. */
|
|
29
|
+
maxTodos?: number;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/planning/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AAExE,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,WAAW;CACb,CAAC"}
|
package/package.json
CHANGED
package/src/core/agent-loop.ts
CHANGED
|
@@ -22,6 +22,8 @@ import type { CheckpointManager } from "../checkpoint/checkpoint-manager.js";
|
|
|
22
22
|
import type { InjectionManager } from "../injection/injection-manager.js";
|
|
23
23
|
import type { ExtensionRunner } from "../extensions/runner.js";
|
|
24
24
|
import type { CompactionConfig } from "../compaction/types.js";
|
|
25
|
+
import type { PlanningManager } from "../planning/planning-manager.js";
|
|
26
|
+
import { PLANNING_TOOL_NAMES } from "../planning/types.js";
|
|
25
27
|
import { executeToolCalls, type ToolCallResult } from "../tools/executor.js";
|
|
26
28
|
import { estimateTokens, shouldCompact, findCutPoint } from "../compaction/compaction.js";
|
|
27
29
|
import { normalizeHistory } from "../injection/history-normalizer.js";
|
|
@@ -50,6 +52,7 @@ export interface AgentLoopConfig {
|
|
|
50
52
|
injectionManager?: InjectionManager;
|
|
51
53
|
extensionRunner?: ExtensionRunner;
|
|
52
54
|
compaction?: CompactionConfig & { summarize?: (messages: AgentMessage[]) => Promise<string> };
|
|
55
|
+
planningManager?: PlanningManager;
|
|
53
56
|
modelCost?: ModelCost;
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -248,8 +251,14 @@ export async function runAgentLoop(
|
|
|
248
251
|
llmMessages = stripImages(llmMessages);
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
// --- Phase-aware tool filtering ---
|
|
255
|
+
let visibleTools = allTools;
|
|
256
|
+
if (config.planningManager?.phase === "planning" && config.planningManager.allowedInPlanning.size > 2) {
|
|
257
|
+
visibleTools = allTools.filter((t) => config.planningManager!.allowedInPlanning.has(t.name));
|
|
258
|
+
}
|
|
259
|
+
|
|
251
260
|
// --- Stream LLM response ---
|
|
252
|
-
const toolDefs = toolsToDefinitions(
|
|
261
|
+
const toolDefs = toolsToDefinitions(visibleTools);
|
|
253
262
|
let requestOptions: LLMRequestOptions = {
|
|
254
263
|
model: modelId,
|
|
255
264
|
systemPrompt,
|
|
@@ -319,7 +328,7 @@ export async function runAgentLoop(
|
|
|
319
328
|
|
|
320
329
|
if (toolCalls.length > 0) {
|
|
321
330
|
const results = await executeToolCalls(toolCalls, {
|
|
322
|
-
tools:
|
|
331
|
+
tools: visibleTools,
|
|
323
332
|
mode: config.toolExecution,
|
|
324
333
|
approval: config.approval,
|
|
325
334
|
agentName: config.agentName,
|
|
@@ -360,6 +369,14 @@ export async function runAgentLoop(
|
|
|
360
369
|
await sessionManager?.appendMessage(rm);
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
// --- Planning: tick round counter (reset happens inside updateTodos when todo tool is called) ---
|
|
373
|
+
if (config.planningManager) {
|
|
374
|
+
const calledTodo = toolCalls.some((tc) => tc.name === PLANNING_TOOL_NAMES.todo);
|
|
375
|
+
if (!calledTodo) {
|
|
376
|
+
config.planningManager.tickRound();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
363
380
|
hasMoreWork = true;
|
|
364
381
|
}
|
|
365
382
|
|
package/src/core/agent.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { SubagentConfig } from "../multi-agent/types.js";
|
|
|
21
21
|
import type { SkillSource } from "../skills/types.js";
|
|
22
22
|
import type { MCPServerConfig, MCPClient } from "../tools/mcp-adapter.js";
|
|
23
23
|
import type { TaskFactory } from "../background/types.js";
|
|
24
|
+
import type { PlanningConfig } from "../planning/types.js";
|
|
24
25
|
import { SessionManager } from "../session/session-manager.js";
|
|
25
26
|
import { CheckpointManager } from "../checkpoint/checkpoint-manager.js";
|
|
26
27
|
import { InjectionManager } from "../injection/injection-manager.js";
|
|
@@ -35,6 +36,9 @@ import { LLMSummarizer, agentMessagesToCompactionInput } from "../compaction/sum
|
|
|
35
36
|
import { Wire } from "../wire/wire.js";
|
|
36
37
|
import { BackgroundTaskManager } from "../background/task-manager.js";
|
|
37
38
|
import { createBackgroundTaskTools } from "../background/tools.js";
|
|
39
|
+
import { PlanningManager } from "../planning/planning-manager.js";
|
|
40
|
+
import { createPlanningTools } from "../planning/tools.js";
|
|
41
|
+
import { PlanningNagProvider } from "../planning/nag-injection.js";
|
|
38
42
|
import { runAgentLoop } from "./agent-loop.js";
|
|
39
43
|
|
|
40
44
|
export interface AgentConfig {
|
|
@@ -60,6 +64,7 @@ export interface AgentConfig {
|
|
|
60
64
|
mcp?: { servers: MCPServerConfig[]; clientFactory: (config: MCPServerConfig) => MCPClient };
|
|
61
65
|
wire?: { bufferSize?: number };
|
|
62
66
|
backgroundTasks?: { factories?: Record<string, TaskFactory> };
|
|
67
|
+
planning?: PlanningConfig;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
export class Agent {
|
|
@@ -83,6 +88,7 @@ export class Agent {
|
|
|
83
88
|
private _mcpAdapter: MCPAdapter | undefined;
|
|
84
89
|
private _wire: Wire;
|
|
85
90
|
private _backgroundTaskManager: BackgroundTaskManager | undefined;
|
|
91
|
+
private _planningManager: PlanningManager | undefined;
|
|
86
92
|
private _initialized = false;
|
|
87
93
|
|
|
88
94
|
constructor(config: AgentConfig) {
|
|
@@ -145,6 +151,11 @@ export class Agent {
|
|
|
145
151
|
}
|
|
146
152
|
});
|
|
147
153
|
}
|
|
154
|
+
|
|
155
|
+
// Planning
|
|
156
|
+
if (config.planning) {
|
|
157
|
+
this._planningManager = new PlanningManager(config.planning);
|
|
158
|
+
}
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
// --- Public API ---
|
|
@@ -238,6 +249,10 @@ export class Agent {
|
|
|
238
249
|
return this._backgroundTaskManager;
|
|
239
250
|
}
|
|
240
251
|
|
|
252
|
+
get planning(): PlanningManager | undefined {
|
|
253
|
+
return this._planningManager;
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
setSystemPrompt(prompt: string): void {
|
|
242
257
|
this._state.systemPrompt = prompt;
|
|
243
258
|
}
|
|
@@ -352,6 +367,19 @@ export class Agent {
|
|
|
352
367
|
const bgTools = createBackgroundTaskTools(this._backgroundTaskManager, this._config.backgroundTasks?.factories);
|
|
353
368
|
this._state.tools = [...this._state.tools, ...bgTools];
|
|
354
369
|
}
|
|
370
|
+
|
|
371
|
+
// Planning tools + nag injection
|
|
372
|
+
if (this._planningManager) {
|
|
373
|
+
this._state.tools = [...this._state.tools, ...createPlanningTools(this._planningManager)];
|
|
374
|
+
|
|
375
|
+
// Register nag provider into injection manager (create one if needed)
|
|
376
|
+
const nagProvider = new PlanningNagProvider(this._planningManager);
|
|
377
|
+
if (this._injectionManager) {
|
|
378
|
+
this._injectionManager.addProvider(nagProvider);
|
|
379
|
+
} else {
|
|
380
|
+
this._injectionManager = new InjectionManager([nagProvider]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
355
383
|
}
|
|
356
384
|
|
|
357
385
|
private _normalizeInput(input: string | AgentMessage | AgentMessage[]): AgentMessage[] {
|
|
@@ -430,6 +458,7 @@ export class Agent {
|
|
|
430
458
|
injectionManager: this._injectionManager,
|
|
431
459
|
extensionRunner: this._extensionRunner,
|
|
432
460
|
compaction: compactionWithSummarizer,
|
|
461
|
+
planningManager: this._planningManager,
|
|
433
462
|
});
|
|
434
463
|
|
|
435
464
|
this._state.messages = result;
|
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
|
|
|
@@ -101,6 +104,9 @@ export { LLMSummarizer } from "./compaction/summarizer.js";
|
|
|
101
104
|
export { Wire } from "./wire/wire.js";
|
|
102
105
|
export { BackgroundTaskManager } from "./background/task-manager.js";
|
|
103
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";
|
|
104
110
|
|
|
105
111
|
// Core types
|
|
106
112
|
export type {
|
|
@@ -227,3 +233,13 @@ export type {
|
|
|
227
233
|
BackgroundTaskEvent,
|
|
228
234
|
TaskFactory,
|
|
229
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";
|
|
@@ -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
|
+
}
|