@noetic-tools/context 1.0.0 → 1.1.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.
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ContextLayer, ContextScope, WorkflowDocument, WorkflowNode } from '@noetic-tools/types';
|
|
2
|
+
import { PlanStyle } from './plan-prompts.js';
|
|
3
|
+
export { PlanStyle } from './plan-prompts.js';
|
|
2
4
|
export declare const PlanPhase: {
|
|
3
5
|
readonly Idle: 'idle';
|
|
4
6
|
readonly Planning: 'planning';
|
|
@@ -51,6 +53,20 @@ export interface PlanContextConfig {
|
|
|
51
53
|
* must include `'subflow'` for named workflows to be referenceable.
|
|
52
54
|
*/
|
|
53
55
|
allowedNodeKinds?: WorkflowNode['kind'][];
|
|
56
|
+
/**
|
|
57
|
+
* How the planning briefing shapes the turn. `phased` (default) marches
|
|
58
|
+
* through explore → design → review → write → exit; `interview` loops
|
|
59
|
+
* explore → write → ask, which suits requests whose requirements are still
|
|
60
|
+
* vague.
|
|
61
|
+
*/
|
|
62
|
+
style?: PlanStyle;
|
|
63
|
+
/**
|
|
64
|
+
* Name of the host's sub-agent tool, when it has one (e.g. `'agent'`). The
|
|
65
|
+
* layer ships no such tool, so the briefing's parallel-exploration guidance
|
|
66
|
+
* stays out until a host names one — telling the model to call a tool that is
|
|
67
|
+
* not registered costs a turn and teaches it nothing.
|
|
68
|
+
*/
|
|
69
|
+
subAgentTool?: string;
|
|
54
70
|
/** Extra free-form instructions appended to the planning-phase recall payload. */
|
|
55
71
|
additionalPlanInstructions?: string;
|
|
56
72
|
/** Called once when the layer transitions Idle → Planning. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createMessage, estimateTokens, Slot, SteeringAction, WorkflowDocumentSchema, walkWorkflow, workflowDepth, } from '@noetic-tools/types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { layerData, layerFn } from '../layer-provides.js';
|
|
4
|
+
import { nodeKindList, PlanStyle, renderExecuting, renderPlanning, renderTerminal, } from './plan-prompts.js';
|
|
5
|
+
export { PlanStyle } from './plan-prompts.js';
|
|
4
6
|
//#region Constants
|
|
5
7
|
const MAX_PRD_LENGTH = 5e4;
|
|
6
8
|
const MAX_DEPTH = 5;
|
|
@@ -224,103 +226,19 @@ function trimExecutionLog(log) {
|
|
|
224
226
|
}
|
|
225
227
|
return log.slice(log.length - MAX_EXECUTION_LOG_ENTRIES);
|
|
226
228
|
}
|
|
227
|
-
/** One-line summary of a workflow: node count and a kind histogram. */
|
|
228
|
-
function summarizeWorkflow(name, doc) {
|
|
229
|
-
const counts = new Map();
|
|
230
|
-
let total = 0;
|
|
231
|
-
for (const node of walkWorkflow(doc.root)) {
|
|
232
|
-
total++;
|
|
233
|
-
counts.set(node.kind, (counts.get(node.kind) ?? 0) + 1);
|
|
234
|
-
}
|
|
235
|
-
const histogram = [
|
|
236
|
-
...counts.entries(),
|
|
237
|
-
]
|
|
238
|
-
.map(([kind, n]) => `${kind} x${n}`)
|
|
239
|
-
.join(', ');
|
|
240
|
-
return `- ${name}: ${total} node${total === 1 ? '' : 's'} (${histogram})`;
|
|
241
|
-
}
|
|
242
|
-
function renderWorkflowSummaries(state) {
|
|
243
|
-
const entries = Object.entries(state.workflows).sort(([a], [b]) => a.localeCompare(b));
|
|
244
|
-
if (entries.length === 0) {
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
return [
|
|
248
|
-
'',
|
|
249
|
-
'## Named Workflows',
|
|
250
|
-
'',
|
|
251
|
-
...entries.map(([name, doc]) => summarizeWorkflow(name, doc)),
|
|
252
|
-
"(Use plan/getWorkflow to review a workflow's JSON before revising it.)",
|
|
253
|
-
];
|
|
254
|
-
}
|
|
255
|
-
//#endregion
|
|
256
|
-
//#region Recall Renderers
|
|
257
|
-
function recallPlanning(state, additionalInstructions) {
|
|
258
|
-
const sections = [
|
|
259
|
-
'<plan_mode>',
|
|
260
|
-
'You are in PLAN MODE. You may only use read-only tools (Read, Grep, Find, Ls), AskUserQuestion, skill activation, sub-agent coordination tools, and requestPlanApproval to explore the codebase and request execution approval.',
|
|
261
|
-
'Your goal is to produce a PRD document and (optionally) a structured execution plan.',
|
|
262
|
-
'',
|
|
263
|
-
'## Workflow (5 phases)',
|
|
264
|
-
'',
|
|
265
|
-
'1. **Initial Understanding** — Read code and gather context. To parallelise exploration, use the `agent` tool to spawn bounded read-only sub-agents; up to 3 in parallel. Each sub-agent returns a focused report.',
|
|
266
|
-
'2. **Design** — Synthesise findings. Optionally spawn planning sub-agents (1–3 in parallel) to draft alternative implementation approaches and surface trade-offs.',
|
|
267
|
-
'3. **Review** — Read the critical files identified by your subagents directly so you understand them first-hand. If anything is ambiguous, ask the user a focused question.',
|
|
268
|
-
'4. **Final Plan** — Write the PRD via `plan/updatePrd`. Lead with a **Context** section (why this change), then your single recommended approach, the paths of files to modify, existing functions/utilities to reuse, and a **Verification** section. Then call `plan/setPlanTree` with `{ "document": { "version": 1, "root": <WorkflowNode> } }` — a JSON workflow conforming to ' +
|
|
269
|
-
SCHEMA_URL +
|
|
270
|
-
'. Node kinds: `llm` (model turn), `tool` (single tool call), `sequence`, `fork` (parallel paths), `branch` (substring routing), `loop` (body + until predicate), `spawn` (isolated child), `subflow` (reference to a named workflow), and coding-agent nodes (`claude-code`, `codex`, `opencode`, `pi`). Every node needs a unique `id`. **Keep the tree SMALL** — aim for at most ~7 top-level nodes the user can review at a glance. Factor detailed mechanics (multi-step verification, retries, fan-out) into NAMED workflows: define each with `plan/setWorkflow` `{ "name": "<slug>", "document": {...} }` and reference it from the tree with `{ "kind": "subflow", "id": "...", "ref": "<slug>" }`. Named workflows may reference other named workflows but must not form cycles.',
|
|
271
|
-
'5. **Exit** — Call `plan/exitPlanMode` with `{ action: "execute" }` to request approval. Every subflow ref must name a defined workflow or the exit is rejected. The user must accept before execution begins; if they reject, you stay in Plan Mode and may revise.',
|
|
272
|
-
'',
|
|
273
|
-
'## Available actions',
|
|
274
|
-
'- `plan/updatePrd` — set markdown PRD content',
|
|
275
|
-
'- `plan/setPlanTree` — set the reviewed plan as a JSON workflow document',
|
|
276
|
-
'- `plan/setWorkflow` — create or replace a named workflow (referenced via subflow nodes)',
|
|
277
|
-
'- `plan/removeWorkflow` — delete a named workflow',
|
|
278
|
-
"- `plan/getWorkflow` — read back a named workflow's JSON",
|
|
279
|
-
'- `plan/exitPlanMode` `{ action: "execute" }` — request approval and exit to executing',
|
|
280
|
-
'- `plan/exitPlanMode` `{ action: "cancel" }` — discard plan and return to idle',
|
|
281
|
-
'',
|
|
282
|
-
'## Constraints',
|
|
283
|
-
'- DO NOT create, modify, or delete files (other than via the plan/* actions).',
|
|
284
|
-
'- DO NOT run mutating shell commands. Read-only exploration only.',
|
|
285
|
-
'- End each turn either by asking the user a focused clarifying question or by calling `plan/exitPlanMode`.',
|
|
286
|
-
];
|
|
287
|
-
if (additionalInstructions) {
|
|
288
|
-
sections.push('', '## Additional Instructions', '', additionalInstructions);
|
|
289
|
-
}
|
|
290
|
-
if (state.prd) {
|
|
291
|
-
sections.push('', '## Current PRD Draft', '', state.prd);
|
|
292
|
-
}
|
|
293
|
-
if (state.planTree) {
|
|
294
|
-
sections.push('', '## Current Plan Tree', '', JSON.stringify(state.planTree, null, 2));
|
|
295
|
-
}
|
|
296
|
-
sections.push(...renderWorkflowSummaries(state));
|
|
297
|
-
sections.push('</plan_mode>');
|
|
298
|
-
return sections.join('\n');
|
|
299
|
-
}
|
|
300
|
-
function recallExecuting(state) {
|
|
301
|
-
const sections = [
|
|
302
|
-
'<active_plan>',
|
|
303
|
-
'## PRD',
|
|
304
|
-
'',
|
|
305
|
-
state.prd ?? '',
|
|
306
|
-
];
|
|
307
|
-
if (state.planTree) {
|
|
308
|
-
sections.push('', '## Execution Plan', '', JSON.stringify(state.planTree, null, 2));
|
|
309
|
-
}
|
|
310
|
-
sections.push(...renderWorkflowSummaries(state));
|
|
311
|
-
sections.push('</active_plan>');
|
|
312
|
-
return sections.join('\n');
|
|
313
|
-
}
|
|
314
|
-
function recallTerminal(state) {
|
|
315
|
-
const lastEntry = state.executionLog[state.executionLog.length - 1];
|
|
316
|
-
const outcome = lastEntry?.outcome ?? 'unknown';
|
|
317
|
-
return `<plan_outcome>Plan v${state.version} ${outcome}.</plan_outcome>`;
|
|
318
|
-
}
|
|
319
229
|
const RECALL_RENDERERS = {
|
|
320
|
-
[PlanPhase.Planning]:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
230
|
+
[PlanPhase.Planning]: (state, options) => renderPlanning(state, {
|
|
231
|
+
style: options.style,
|
|
232
|
+
schemaUrl: SCHEMA_URL,
|
|
233
|
+
allowedTools: options.allowedTools,
|
|
234
|
+
allowedNodeKinds: options.allowedNodeKinds,
|
|
235
|
+
subAgentTool: options.subAgentTool,
|
|
236
|
+
extra: options.extra,
|
|
237
|
+
maxChars: options.maxChars,
|
|
238
|
+
}),
|
|
239
|
+
[PlanPhase.Executing]: (state, options) => renderExecuting(state, options.maxChars),
|
|
240
|
+
[PlanPhase.Completed]: (state, options) => renderTerminal(state, options.maxChars),
|
|
241
|
+
[PlanPhase.Failed]: (state, options) => renderTerminal(state, options.maxChars),
|
|
324
242
|
};
|
|
325
243
|
//#endregion
|
|
326
244
|
//#region Public API
|
|
@@ -346,6 +264,8 @@ export function planContext(config) {
|
|
|
346
264
|
allowedNodeKinds: config?.allowedNodeKinds,
|
|
347
265
|
};
|
|
348
266
|
const allowedTools = buildAllowedTools(config);
|
|
267
|
+
const style = config?.style ?? PlanStyle.Phased;
|
|
268
|
+
const subAgentTool = config?.subAgentTool;
|
|
349
269
|
const additionalPlanInstructions = config?.additionalPlanInstructions;
|
|
350
270
|
const onEnterSession = config?.onEnterSession;
|
|
351
271
|
const onExit = config?.onExit;
|
|
@@ -427,7 +347,9 @@ export function planContext(config) {
|
|
|
427
347
|
}),
|
|
428
348
|
setPlanTree: layerFn({
|
|
429
349
|
description: 'Set the execution plan as a JSON workflow document: { "document": { "version": 1, "root": <WorkflowNode> } }. ' +
|
|
430
|
-
|
|
350
|
+
// Drawn from the same table as the briefing's kind list, so the two
|
|
351
|
+
// can never disagree about what this plan is allowed to use.
|
|
352
|
+
`WorkflowNode is a discriminated union on "kind" (${nodeKindList(config?.allowedNodeKinds)}); every node needs a unique "id". ` +
|
|
431
353
|
'Keep this tree SMALL — reference named workflows with { "kind": "subflow", "id": "...", "ref": "<workflow-name>" } and define them via plan/setWorkflow. ' +
|
|
432
354
|
`Schema: ${SCHEMA_URL}`,
|
|
433
355
|
// The document is validated with WorkflowDocumentSchema inside execute
|
|
@@ -682,16 +604,28 @@ export function planContext(config) {
|
|
|
682
604
|
if (!renderer) {
|
|
683
605
|
return null;
|
|
684
606
|
}
|
|
685
|
-
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
607
|
+
// estimateTokens counts ~4 chars per token, so budget*4 chars is the
|
|
608
|
+
// ceiling the renderer must fit under.
|
|
609
|
+
const content = renderer(state, {
|
|
610
|
+
style,
|
|
611
|
+
allowedTools: [
|
|
612
|
+
...allowedTools,
|
|
613
|
+
].sort(),
|
|
614
|
+
allowedNodeKinds: limits.allowedNodeKinds,
|
|
615
|
+
subAgentTool,
|
|
616
|
+
extra: additionalPlanInstructions,
|
|
617
|
+
maxChars: budget * 4,
|
|
618
|
+
});
|
|
619
|
+
// A budget too small for even the compact briefing buys nothing but a
|
|
620
|
+
// fragment of a rule, so the layer sits the turn out.
|
|
621
|
+
if (content === null) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
690
624
|
return {
|
|
691
625
|
items: [
|
|
692
|
-
createMessage(
|
|
626
|
+
createMessage(content, 'developer'),
|
|
693
627
|
],
|
|
694
|
-
tokenCount: estimateTokens(
|
|
628
|
+
tokenCount: estimateTokens(content),
|
|
695
629
|
};
|
|
696
630
|
},
|
|
697
631
|
async beforeToolCall({ toolName, state }) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text the plan layer puts in front of the model: the plan-mode briefing
|
|
3
|
+
* during planning, the active-plan reminder during execution, and the outcome
|
|
4
|
+
* line after it ends.
|
|
5
|
+
*
|
|
6
|
+
* Two planning styles are offered, mirroring how a planning turn is actually
|
|
7
|
+
* spent. `phased` marches through explore → design → review → write → exit and
|
|
8
|
+
* suits work whose shape is already known. `interview` loops explore → write →
|
|
9
|
+
* ask and suits vague requests, where the user is the missing input.
|
|
10
|
+
*
|
|
11
|
+
* Rendering is pure: every function takes state and returns a string, or null
|
|
12
|
+
* when the budget is too small to say anything coherent. The layer owns which
|
|
13
|
+
* one runs for a given phase.
|
|
14
|
+
*/
|
|
15
|
+
import type { WorkflowNode } from '@noetic-tools/types';
|
|
16
|
+
import type { PlanState } from './plan-context.js';
|
|
17
|
+
/** How the planning briefing shapes the turn. */
|
|
18
|
+
export declare const PlanStyle: {
|
|
19
|
+
/** Explore → design → review → write → exit. Best when the work's shape is known. */
|
|
20
|
+
readonly Phased: 'phased';
|
|
21
|
+
/** Explore → write → ask, on repeat. Best when requirements are still vague. */
|
|
22
|
+
readonly Interview: 'interview';
|
|
23
|
+
};
|
|
24
|
+
export type PlanStyle = (typeof PlanStyle)[keyof typeof PlanStyle];
|
|
25
|
+
export interface PlanningPromptOptions {
|
|
26
|
+
style: PlanStyle;
|
|
27
|
+
schemaUrl: string;
|
|
28
|
+
/** Every tool the layer will let through, so the briefing names what the host actually permits. */
|
|
29
|
+
allowedTools: string[];
|
|
30
|
+
/** Node kinds the layer will accept. Undefined means all of them. */
|
|
31
|
+
allowedNodeKinds?: WorkflowNode['kind'][];
|
|
32
|
+
/**
|
|
33
|
+
* Name of the host's sub-agent tool, when it has one. The layer ships no such
|
|
34
|
+
* tool, so the parallel-exploration guidance stays out of the briefing until a
|
|
35
|
+
* host says the tool exists — instructions to call a tool that is not
|
|
36
|
+
* registered cost a turn and teach the model nothing.
|
|
37
|
+
*/
|
|
38
|
+
subAgentTool?: string;
|
|
39
|
+
/** Extra host instructions appended to the briefing. */
|
|
40
|
+
extra?: string;
|
|
41
|
+
/** Character budget for the whole block. */
|
|
42
|
+
maxChars: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The plan-mode briefing: what the model may do, how to spend the turn, and
|
|
46
|
+
* what it has so far. Returns null when even the compact briefing overflows —
|
|
47
|
+
* half a rule is worse than no rule.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderPlanning(state: PlanState, options: PlanningPromptOptions): string | null;
|
|
50
|
+
/** The approved plan, carried into execution so the model works from it rather than from memory. */
|
|
51
|
+
export declare function renderExecuting(state: PlanState, maxChars: number): string | null;
|
|
52
|
+
/** One line saying how the last execution ended. */
|
|
53
|
+
export declare function renderTerminal(state: PlanState, maxChars: number): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* The node kinds a plan may use, as a prose list. Shared with `setPlanTree`'s
|
|
56
|
+
* tool description so the briefing and the tool never disagree about what is
|
|
57
|
+
* allowed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function nodeKindList(allowed?: WorkflowNode['kind'][]): string;
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text the plan layer puts in front of the model: the plan-mode briefing
|
|
3
|
+
* during planning, the active-plan reminder during execution, and the outcome
|
|
4
|
+
* line after it ends.
|
|
5
|
+
*
|
|
6
|
+
* Two planning styles are offered, mirroring how a planning turn is actually
|
|
7
|
+
* spent. `phased` marches through explore → design → review → write → exit and
|
|
8
|
+
* suits work whose shape is already known. `interview` loops explore → write →
|
|
9
|
+
* ask and suits vague requests, where the user is the missing input.
|
|
10
|
+
*
|
|
11
|
+
* Rendering is pure: every function takes state and returns a string, or null
|
|
12
|
+
* when the budget is too small to say anything coherent. The layer owns which
|
|
13
|
+
* one runs for a given phase.
|
|
14
|
+
*/
|
|
15
|
+
import { walkWorkflow } from '@noetic-tools/types';
|
|
16
|
+
//#region Types
|
|
17
|
+
/** How the planning briefing shapes the turn. */
|
|
18
|
+
export const PlanStyle = {
|
|
19
|
+
/** Explore → design → review → write → exit. Best when the work's shape is known. */
|
|
20
|
+
Phased: 'phased',
|
|
21
|
+
/** Explore → write → ask, on repeat. Best when requirements are still vague. */
|
|
22
|
+
Interview: 'interview',
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region Public API
|
|
26
|
+
/**
|
|
27
|
+
* The plan-mode briefing: what the model may do, how to spend the turn, and
|
|
28
|
+
* what it has so far. Returns null when even the compact briefing overflows —
|
|
29
|
+
* half a rule is worse than no rule.
|
|
30
|
+
*/
|
|
31
|
+
export function renderPlanning(state, options) {
|
|
32
|
+
const turn = options.style === PlanStyle.Interview
|
|
33
|
+
? interviewTurn(options.subAgentTool)
|
|
34
|
+
: phasedTurn(options.subAgentTool);
|
|
35
|
+
const blocks = [
|
|
36
|
+
{
|
|
37
|
+
text: [
|
|
38
|
+
'<plan_mode>',
|
|
39
|
+
PREAMBLE,
|
|
40
|
+
'',
|
|
41
|
+
tools(options.allowedTools),
|
|
42
|
+
'',
|
|
43
|
+
turn,
|
|
44
|
+
'',
|
|
45
|
+
PRD_GUIDE,
|
|
46
|
+
'',
|
|
47
|
+
treeGuide(options.schemaUrl, options.allowedNodeKinds),
|
|
48
|
+
'',
|
|
49
|
+
ACTIONS,
|
|
50
|
+
'',
|
|
51
|
+
ENDING_A_TURN,
|
|
52
|
+
].join('\n'),
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
if (options.extra) {
|
|
56
|
+
blocks.push({
|
|
57
|
+
text: `\n## Additional instructions\n\n${options.extra}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (state.prd) {
|
|
61
|
+
blocks.push({
|
|
62
|
+
text: `\n## Current PRD draft\n\n${state.prd}`,
|
|
63
|
+
drop: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (state.planTree) {
|
|
67
|
+
blocks.push({
|
|
68
|
+
text: `\n## Current plan tree\n\n${JSON.stringify(state.planTree, null, 2)}`,
|
|
69
|
+
drop: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const summaries = workflowSummaries(state);
|
|
73
|
+
if (summaries) {
|
|
74
|
+
blocks.push({
|
|
75
|
+
text: `\n${summaries}`,
|
|
76
|
+
drop: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return fit(blocks, {
|
|
80
|
+
closingTag: '</plan_mode>',
|
|
81
|
+
maxChars: options.maxChars,
|
|
82
|
+
compact: COMPACT_PLANNING,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** The approved plan, carried into execution so the model works from it rather than from memory. */
|
|
86
|
+
export function renderExecuting(state, maxChars) {
|
|
87
|
+
const blocks = [
|
|
88
|
+
{
|
|
89
|
+
text: `<active_plan>\nThe plan below was approved and is now running. This is the work it describes; do not substitute a different approach for it. If what you find contradicts the plan, say so plainly in your output rather than quietly planning around it.\n\n## PRD\n\n${state.prd ?? ''}`,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
if (state.planTree) {
|
|
93
|
+
blocks.push({
|
|
94
|
+
text: `\n## Execution plan\n\n${JSON.stringify(state.planTree, null, 2)}`,
|
|
95
|
+
drop: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const summaries = workflowSummaries(state);
|
|
99
|
+
if (summaries) {
|
|
100
|
+
blocks.push({
|
|
101
|
+
text: `\n${summaries}`,
|
|
102
|
+
drop: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return fit(blocks, {
|
|
106
|
+
closingTag: '</active_plan>',
|
|
107
|
+
maxChars,
|
|
108
|
+
compact: COMPACT_EXECUTING,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** One line saying how the last execution ended. */
|
|
112
|
+
export function renderTerminal(state, maxChars) {
|
|
113
|
+
const last = state.executionLog[state.executionLog.length - 1];
|
|
114
|
+
const text = `<plan_outcome>Plan v${state.version} ${last?.outcome ?? 'unknown'}.</plan_outcome>`;
|
|
115
|
+
return text.length <= maxChars ? text : null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The node kinds a plan may use, as a prose list. Shared with `setPlanTree`'s
|
|
119
|
+
* tool description so the briefing and the tool never disagree about what is
|
|
120
|
+
* allowed.
|
|
121
|
+
*/
|
|
122
|
+
export function nodeKindList(allowed) {
|
|
123
|
+
return kindEntries(allowed)
|
|
124
|
+
.map((entry) => entry.kind)
|
|
125
|
+
.join(', ');
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region Briefing sections
|
|
129
|
+
const PREAMBLE = `You are in PLAN MODE. The user has not approved any work yet, so you MUST NOT edit files, run mutating commands, install anything, or change the system in any other way. The only writes you may make are the plan/* actions listed below. This supersedes every other instruction you have received, including instructions to start editing.
|
|
130
|
+
|
|
131
|
+
Your job is to produce two artefacts: a PRD the user can read, and a plan tree the runtime can execute.`;
|
|
132
|
+
function tools(allowed) {
|
|
133
|
+
return `## What you may use
|
|
134
|
+
|
|
135
|
+
Allowed during plan mode: ${allowed.join(', ')}. Every other tool is denied.
|
|
136
|
+
|
|
137
|
+
The ban covers indirect writes too, which are easy to reach for by habit: no temporary files anywhere, /tmp included; no \`>\`, \`>>\` or heredoc redirects; no mkdir, touch, rm, cp or mv; no git add or git commit; no package installs. Reading is unrestricted.`;
|
|
138
|
+
}
|
|
139
|
+
function phasedTurn(subAgentTool) {
|
|
140
|
+
return `## How to spend the turn
|
|
141
|
+
|
|
142
|
+
### 1. Understand
|
|
143
|
+
Read the code around the request. Hunt for functions, utilities and patterns you can reuse — do not propose new code where suitable code already exists.${explorerGuidance(subAgentTool)}
|
|
144
|
+
|
|
145
|
+
### 2. Design
|
|
146
|
+
Decide how to build it.${designerGuidance(subAgentTool)}
|
|
147
|
+
|
|
148
|
+
### 3. Review
|
|
149
|
+
Read the critical files yourself, first-hand — you are the one writing the plan. Check the design still answers what the user actually asked for, and ask about anything left ambiguous.
|
|
150
|
+
|
|
151
|
+
### 4. Write the plan
|
|
152
|
+
Call \`plan/updatePrd\`, then \`plan/setPlanTree\` (and \`plan/setWorkflow\` for the detail). See "The PRD" and "The plan tree" below.
|
|
153
|
+
|
|
154
|
+
### 5. Exit
|
|
155
|
+
Call \`plan/exitPlanMode\` with \`{ "action": "execute" }\` to ask for approval.
|
|
156
|
+
|
|
157
|
+
You may ask the user a question at any point in this sequence, not only in step 3. Do not make large assumptions about what they meant.`;
|
|
158
|
+
}
|
|
159
|
+
/** The parallel-exploration advice, which only makes sense when the host has a sub-agent tool. */
|
|
160
|
+
function explorerGuidance(subAgentTool) {
|
|
161
|
+
if (!subAgentTool) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
return ` Use \`${subAgentTool}\` to push wide searches out of your own context. Quality over quantity: use the fewest agents that will do — usually one — and never more than three. One agent is right when the request names its files or the change is small and targeted; reach for more only when the scope is uncertain or several areas of the codebase are involved. Give each agent a distinct area so their reports do not overlap.`;
|
|
165
|
+
}
|
|
166
|
+
function designerGuidance(subAgentTool) {
|
|
167
|
+
if (!subAgentTool) {
|
|
168
|
+
return ' Weigh the obvious alternatives and pick one. For a large or risky change, write down the trade-off you are making so the user can disagree with it.';
|
|
169
|
+
}
|
|
170
|
+
return ` Launch at least one \`${subAgentTool}\` planning agent for anything non-trivial — it validates your understanding and surfaces alternatives you did not consider. Skip it only for typo fixes, single-line changes and simple renames. Up to three in parallel, each with a different perspective, when the task touches several parts of the codebase or has many edge cases: simplicity vs. performance vs. maintainability for a feature; root cause vs. workaround vs. prevention for a bug; minimal change vs. clean architecture for a refactor.
|
|
171
|
+
|
|
172
|
+
In the agent's prompt: give it the background you gathered, naming the files and the code paths you traced; state the requirements and constraints; ask for a detailed implementation plan that ends with a **Critical Files for Implementation** list of the 3–5 files most central to the work. You will read those files yourself in step 3, so the list is what makes the next step possible.`;
|
|
173
|
+
}
|
|
174
|
+
function interviewTurn(subAgentTool) {
|
|
175
|
+
const parallel = subAgentTool
|
|
176
|
+
? ` Push wide searches out to \`${subAgentTool}\` when they would otherwise fill your context.`
|
|
177
|
+
: '';
|
|
178
|
+
return `## How to spend the turn
|
|
179
|
+
|
|
180
|
+
You are pair-planning with the user. Explore, write down what you learn, and ask when the code cannot tell you. Repeat until the plan is complete.
|
|
181
|
+
|
|
182
|
+
1. **Explore** — read code. Look for functions, utilities and patterns to reuse.${parallel}
|
|
183
|
+
2. **Write it down** — call \`plan/updatePrd\` after each round of discovery. Do not save it all for the end; the PRD grows from a skeleton into the finished document.
|
|
184
|
+
3. **Ask** — when you hit a decision the code cannot settle, call AskUserQuestion, then go back to step 1.
|
|
185
|
+
|
|
186
|
+
Start by scanning a few key files, writing a skeleton PRD (headers and rough notes), and asking your first round of questions. Do not explore exhaustively before involving the user.
|
|
187
|
+
|
|
188
|
+
**Asking well.** Never ask what you could find out by reading. Batch related questions into one call. Ask only about things the user owns: requirements, preferences, trade-offs, which edge cases matter. Scale the number of rounds to the request — a vague feature needs several, a focused bug fix may need none.
|
|
189
|
+
|
|
190
|
+
**Converging.** The plan is ready when nothing is ambiguous and it says what changes, which files change, what existing code it reuses, and how to verify it. Then set the plan tree and call \`plan/exitPlanMode\` with \`{ "action": "execute" }\`.`;
|
|
191
|
+
}
|
|
192
|
+
const PRD_GUIDE = `## The PRD
|
|
193
|
+
|
|
194
|
+
Write it with \`plan/updatePrd\` as markdown, structured with headers:
|
|
195
|
+
|
|
196
|
+
- Open with **Context**: why this change is being made — the problem it solves and the outcome intended.
|
|
197
|
+
- Give your recommended approach only. The alternatives you rejected are not the user's problem.
|
|
198
|
+
- List the paths of the files to be modified.
|
|
199
|
+
- Name the existing functions and utilities to reuse, with their paths.
|
|
200
|
+
- Close with **Verification**: how to prove the change works end to end — the command to run, the tests to run, the tools to drive.
|
|
201
|
+
|
|
202
|
+
Keep it scannable but executable. A reader should be able to skim the headers and know the shape of the work.`;
|
|
203
|
+
/** One line per node kind, split into the work itself and the shape it runs in. */
|
|
204
|
+
const KIND_ENTRIES = [
|
|
205
|
+
{
|
|
206
|
+
kind: 'llm',
|
|
207
|
+
group: 'leaf',
|
|
208
|
+
line: '`llm` — a model turn, driven by `instructions`; give it `tools` if it needs them',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
kind: 'tool',
|
|
212
|
+
group: 'leaf',
|
|
213
|
+
line: '`tool` — one deterministic tool call: `toolName` plus `args`',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
kind: 'run',
|
|
217
|
+
group: 'leaf',
|
|
218
|
+
line: '`run` — code in `execute`, run by a subprocess adapter the host must provide',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
kind: 'claude-code',
|
|
222
|
+
group: 'leaf',
|
|
223
|
+
line: '`claude-code`, `codex`, `opencode`, `pi` — hand a `prompt` to a coding agent',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
kind: 'sequence',
|
|
227
|
+
group: 'structure',
|
|
228
|
+
line: '`sequence` — `steps` run in order, each fed the one before',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
kind: 'fork',
|
|
232
|
+
group: 'structure',
|
|
233
|
+
line: '`fork` — `paths` run at once (or `each` over a runtime array); `mode` is `all`, `race` or `settle`, and `merge` says how the outputs of an `all` come back together',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
kind: 'branch',
|
|
237
|
+
group: 'structure',
|
|
238
|
+
line: '`branch` — the FIRST route whose `match` appears in the input wins, case-insensitively, so order the routes narrowest first; `default` catches the rest',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
kind: 'loop',
|
|
242
|
+
group: 'structure',
|
|
243
|
+
line: '`loop` — run `body`, then repeat while the `until` predicate does not hold; the body always runs at least once',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
kind: 'every',
|
|
247
|
+
group: 'structure',
|
|
248
|
+
line: '`every` — run `step` on an interval of `ms`, forever; nothing after it ever runs, so put it last',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
kind: 'spawn',
|
|
252
|
+
group: 'structure',
|
|
253
|
+
line: '`spawn` — run `child` in an isolated context, so its work does not crowd this one',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
kind: 'provide',
|
|
257
|
+
group: 'structure',
|
|
258
|
+
line: '`provide` — run `child` with a named set of memory layers the host must have registered',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
kind: 'subflow',
|
|
262
|
+
group: 'structure',
|
|
263
|
+
line: '`subflow` — run a named workflow by `ref`',
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
/** The harness kinds share one line, so they are folded into the `claude-code` entry. */
|
|
267
|
+
const HARNESS_KINDS = [
|
|
268
|
+
'claude-code',
|
|
269
|
+
'codex',
|
|
270
|
+
'opencode',
|
|
271
|
+
'pi',
|
|
272
|
+
];
|
|
273
|
+
function kindEntries(allowed) {
|
|
274
|
+
if (!allowed) {
|
|
275
|
+
return KIND_ENTRIES;
|
|
276
|
+
}
|
|
277
|
+
const permitted = new Set(allowed);
|
|
278
|
+
return KIND_ENTRIES.filter((entry) => entry.kind === 'claude-code'
|
|
279
|
+
? HARNESS_KINDS.some((kind) => permitted.has(kind))
|
|
280
|
+
: permitted.has(entry.kind));
|
|
281
|
+
}
|
|
282
|
+
function treeGuide(schemaUrl, allowed) {
|
|
283
|
+
const entries = kindEntries(allowed);
|
|
284
|
+
const group = (name) => entries.filter((entry) => entry.group === name).map((entry) => `- ${entry.line}`);
|
|
285
|
+
const leaves = group('leaf');
|
|
286
|
+
const structure = group('structure');
|
|
287
|
+
const restricted = allowed
|
|
288
|
+
? '\n\nThis plan is restricted to the kinds listed above. Any other kind is rejected.'
|
|
289
|
+
: '';
|
|
290
|
+
return `## The plan tree
|
|
291
|
+
|
|
292
|
+
Set it with \`plan/setPlanTree\` as \`{ "document": { "version": 1, "root": <node> } }\`, conforming to ${schemaUrl}. Every node needs a unique \`id\` and a \`kind\`.${restricted}
|
|
293
|
+
|
|
294
|
+
**Leaves — the work itself**
|
|
295
|
+
${leaves.join('\n')}
|
|
296
|
+
|
|
297
|
+
**Structure — how the leaves are arranged**
|
|
298
|
+
${structure.join('\n')}
|
|
299
|
+
|
|
300
|
+
**Shaping it**
|
|
301
|
+
- Keep the tree the user reviews SMALL — about seven top-level nodes, readable at a glance. Push the mechanics down.
|
|
302
|
+
- Factor detail into named workflows: define each with \`plan/setWorkflow\` \`{ "name": "<slug>", "document": {...} }\` and point at it with \`{ "kind": "subflow", "id": "...", "ref": "<slug>" }\`. Named workflows may reference each other but must not form a cycle, and every ref must resolve or the exit is rejected.
|
|
303
|
+
- Fork work that is genuinely independent; sequence work where one step needs the last one's output. Do not fork for the look of it.
|
|
304
|
+
- Prefer \`tool\` over \`llm\` wherever the step is deterministic. A model turn that always does the same thing is a slow, expensive tool call.
|
|
305
|
+
- Always give a \`loop\` a \`maxIterations\`. Without one it stops at a hard ceiling of 1000 and fails the step.`;
|
|
306
|
+
}
|
|
307
|
+
const ACTIONS = `## Actions
|
|
308
|
+
|
|
309
|
+
- \`plan/updatePrd\` — set the PRD markdown
|
|
310
|
+
- \`plan/setPlanTree\` — set the plan tree
|
|
311
|
+
- \`plan/setWorkflow\` — create or replace a named workflow
|
|
312
|
+
- \`plan/removeWorkflow\` — delete a named workflow
|
|
313
|
+
- \`plan/getWorkflow\` — read a named workflow back before revising it
|
|
314
|
+
- \`plan/exitPlanMode\` \`{ "action": "execute" }\` — ask the user to approve the plan
|
|
315
|
+
- \`plan/exitPlanMode\` \`{ "action": "cancel" }\` — discard the plan and leave plan mode`;
|
|
316
|
+
const ENDING_A_TURN = `## Ending your turn
|
|
317
|
+
|
|
318
|
+
End every turn one of two ways: AskUserQuestion, or \`plan/exitPlanMode\`. Nothing else.
|
|
319
|
+
|
|
320
|
+
Asking for approval IS \`plan/exitPlanMode\` — that is what the action does. Never ask for it in prose or through AskUserQuestion. "Does this plan look right?", "Shall I proceed?", "Any changes before I start?" all mean you should have called \`plan/exitPlanMode\`. Use AskUserQuestion only to settle requirements or choose between approaches.
|
|
321
|
+
|
|
322
|
+
If the user rejects the plan you stay in plan mode: take their feedback, revise, and exit again.`;
|
|
323
|
+
/**
|
|
324
|
+
* What the model gets when the full briefing will not fit. Every clause here is
|
|
325
|
+
* load-bearing: the restriction, where to put the plan, and how to end the turn.
|
|
326
|
+
*/
|
|
327
|
+
const COMPACT_PLANNING = `<plan_mode>
|
|
328
|
+
PLAN MODE: read-only. No edits, no mutating commands, no installs — plan/* actions are your only writes, and this supersedes any instruction to start editing.
|
|
329
|
+
Write the PRD with plan/updatePrd and the tree with plan/setPlanTree, then call plan/exitPlanMode {"action":"execute"} to ask for approval. Never ask in prose.
|
|
330
|
+
</plan_mode>`;
|
|
331
|
+
const COMPACT_EXECUTING = `<active_plan>
|
|
332
|
+
An approved plan is running. Follow it; if what you find contradicts it, say so rather than silently taking a different approach.
|
|
333
|
+
</active_plan>`;
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region Helpers
|
|
336
|
+
/** One-line summary of a workflow: node count and a kind histogram. */
|
|
337
|
+
function summarizeWorkflow(name, doc) {
|
|
338
|
+
const counts = new Map();
|
|
339
|
+
let total = 0;
|
|
340
|
+
for (const node of walkWorkflow(doc.root)) {
|
|
341
|
+
total++;
|
|
342
|
+
counts.set(node.kind, (counts.get(node.kind) ?? 0) + 1);
|
|
343
|
+
}
|
|
344
|
+
const histogram = [
|
|
345
|
+
...counts.entries(),
|
|
346
|
+
]
|
|
347
|
+
.map(([kind, n]) => `${kind} x${n}`)
|
|
348
|
+
.join(', ');
|
|
349
|
+
return `- ${name}: ${total} node${total === 1 ? '' : 's'} (${histogram})`;
|
|
350
|
+
}
|
|
351
|
+
function workflowSummaries(state) {
|
|
352
|
+
const entries = Object.entries(state.workflows).sort(([a], [b]) => a.localeCompare(b));
|
|
353
|
+
if (entries.length === 0) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return [
|
|
357
|
+
'## Named workflows',
|
|
358
|
+
'',
|
|
359
|
+
...entries.map(([name, doc]) => summarizeWorkflow(name, doc)),
|
|
360
|
+
'',
|
|
361
|
+
"(Read a workflow's JSON with plan/getWorkflow before revising it.)",
|
|
362
|
+
].join('\n');
|
|
363
|
+
}
|
|
364
|
+
/** Below this, a trimmed state block is a stub the model cannot use, so it goes entirely. */
|
|
365
|
+
const MIN_USEFUL_BLOCK = 400;
|
|
366
|
+
const TRIM_MARK = '\n…(trimmed to fit the context budget)';
|
|
367
|
+
/**
|
|
368
|
+
* Joins the blocks under a character budget.
|
|
369
|
+
*
|
|
370
|
+
* State blocks give way first, fattest first, and are trimmed to whatever
|
|
371
|
+
* headroom is left rather than dropped whole — a truncated PRD draft is worth
|
|
372
|
+
* far more than the blank space dropping it leaves behind. Rules are never cut
|
|
373
|
+
* mid-sentence: if they do not fit, the compact briefing replaces the lot, and
|
|
374
|
+
* if even that overflows the layer says nothing at all.
|
|
375
|
+
*/
|
|
376
|
+
function fit(blocks, into) {
|
|
377
|
+
const { closingTag, maxChars, compact } = into;
|
|
378
|
+
const tail = `\n${closingTag}`;
|
|
379
|
+
if (maxChars < tail.length) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const budget = maxChars - tail.length;
|
|
383
|
+
const kept = [
|
|
384
|
+
...blocks,
|
|
385
|
+
];
|
|
386
|
+
const total = () => kept.reduce((sum, block) => sum + block.text.length, 0);
|
|
387
|
+
while (total() > budget) {
|
|
388
|
+
const fattest = fattestDroppable(kept);
|
|
389
|
+
if (fattest < 0) {
|
|
390
|
+
return compact.length <= maxChars ? compact : null;
|
|
391
|
+
}
|
|
392
|
+
const block = kept[fattest];
|
|
393
|
+
const headroom = budget - (total() - block.text.length);
|
|
394
|
+
if (headroom >= MIN_USEFUL_BLOCK) {
|
|
395
|
+
block.text = `${truncate(block.text, headroom - TRIM_MARK.length)}${TRIM_MARK}`;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
kept.splice(fattest, 1);
|
|
399
|
+
}
|
|
400
|
+
return `${kept.map((block) => block.text).join('')}${tail}`;
|
|
401
|
+
}
|
|
402
|
+
function fattestDroppable(blocks) {
|
|
403
|
+
let fattest = -1;
|
|
404
|
+
for (const [index, block] of blocks.entries()) {
|
|
405
|
+
if (block.drop && (fattest < 0 || block.text.length > blocks[fattest].text.length)) {
|
|
406
|
+
fattest = index;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return fattest;
|
|
410
|
+
}
|
|
411
|
+
/** Cuts to `max` characters without splitting a surrogate pair in half. */
|
|
412
|
+
function truncate(text, max) {
|
|
413
|
+
if (text.length <= max) {
|
|
414
|
+
return text;
|
|
415
|
+
}
|
|
416
|
+
const code = text.charCodeAt(max - 1);
|
|
417
|
+
const isHighSurrogate = code >= 0xd800 && code <= 0xdbff;
|
|
418
|
+
return text.slice(0, isHighSurrogate ? max - 1 : max);
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/context",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Context layer system for Noetic agents: the ContextLayer contract, lifecycle, budget/projection machinery, and built-in layer implementations.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"zod": "^4.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@noetic-tools/types": "^
|
|
47
|
+
"@noetic-tools/types": "^3.0.0",
|
|
48
48
|
"zod": "^4.0.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|