@chantier/core 0.3.0 → 0.5.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/dist/index.d.mts +73 -4
- package/dist/index.mjs +168 -14
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApprovalDetail, ApprovalSink, PermissionEngine } from "@chantier/permissions";
|
|
1
|
+
import { ApprovalDetail, ApprovalSink, PermissionEngine, PermissionRules } from "@chantier/permissions";
|
|
2
2
|
//#region src/types.d.ts
|
|
3
3
|
interface TextBlock {
|
|
4
4
|
type: "text";
|
|
@@ -273,8 +273,32 @@ export declare function compactConversation(opts: CompactConversationOptions): P
|
|
|
273
273
|
export declare function compactedSummaryMessage(summary: string): UserMessage;
|
|
274
274
|
//#endregion
|
|
275
275
|
//#region src/context.d.ts
|
|
276
|
-
/**
|
|
277
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Per-model-family prompt profile. Only the identity section is
|
|
278
|
+
* profile-specific; every other section is shared, fixed-order text.
|
|
279
|
+
*/
|
|
280
|
+
interface ModelProfile {
|
|
281
|
+
/** Family name, for diagnostics and tests. */
|
|
282
|
+
name: string;
|
|
283
|
+
/** Replaces the default identity paragraph when present. */
|
|
284
|
+
identity?: string;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Profile registry: family prefix match on the model id — `glm-` and `claude-`
|
|
288
|
+
* resolve their profiles, anything else the default. Prefixes match the leading
|
|
289
|
+
* id so suffixed tags (e.g. `glm-5.3-flash:cloud`) still resolve to the family.
|
|
290
|
+
*/
|
|
291
|
+
export declare function resolveModelProfile(model: string): ModelProfile;
|
|
292
|
+
/**
|
|
293
|
+
* Builds the system prompt from fixed, blank-line-joined sections: environment,
|
|
294
|
+
* identity (profile-adjustable), doing-tasks rules, denial rule, delegation
|
|
295
|
+
* (only when a `task` tool is offered), tool catalog, tool rules, and every
|
|
296
|
+
* AGENTS.md from cwd up to the git root last.
|
|
297
|
+
*
|
|
298
|
+
* `profile` omitted means the default profile: identity and tool rules keep
|
|
299
|
+
* today's semantics, new sections are purely additive.
|
|
300
|
+
*/
|
|
301
|
+
export declare function buildSystemPrompt(cwd: string, tools: ToolDefinition[], profile?: ModelProfile): Promise<string>;
|
|
278
302
|
//#endregion
|
|
279
303
|
//#region src/session.d.ts
|
|
280
304
|
export declare const DEFAULT_SESSIONS_ROOT: string;
|
|
@@ -350,4 +374,49 @@ export declare function compactSession(opts: CompactSessionOptions): Promise<Com
|
|
|
350
374
|
/** `--continue`: newest session file (by mtime) in this cwd's session dir. */
|
|
351
375
|
export declare function loadNewestSessionId(cwd: string, sessionsRoot?: string): Promise<string | null>;
|
|
352
376
|
//#endregion
|
|
353
|
-
|
|
377
|
+
//#region src/subagent.d.ts
|
|
378
|
+
interface SubagentDeps {
|
|
379
|
+
/** The parent's model adapter; the child runs on the same provider/model seam. */
|
|
380
|
+
adapter: ModelAdapter;
|
|
381
|
+
/** The parent's settings rules, verbatim: deny rules bind the child, allow rules pre-approve. */
|
|
382
|
+
rules: PermissionRules;
|
|
383
|
+
/** The parent's approval sink: a child ask surfaces in the parent's approval UI. */
|
|
384
|
+
sink: ApprovalSink;
|
|
385
|
+
/** Provider label recorded in the child session header. */
|
|
386
|
+
provider: string;
|
|
387
|
+
model: string;
|
|
388
|
+
/** Child turn cap; defaults to 25. */
|
|
389
|
+
maxTurns?: number;
|
|
390
|
+
/**
|
|
391
|
+
* Declared model context window; opts the child into compaction (same
|
|
392
|
+
* contract as runAgent). Undefined keeps compaction off for the child.
|
|
393
|
+
*/
|
|
394
|
+
contextWindow?: number;
|
|
395
|
+
/**
|
|
396
|
+
* The child's toolset. Phase A depth cap: the child toolset is the builtin
|
|
397
|
+
* set, which does not contain `task`, so a child cannot recurse by
|
|
398
|
+
* construction.
|
|
399
|
+
*/
|
|
400
|
+
tools: ToolDefinition[];
|
|
401
|
+
}
|
|
402
|
+
interface SubagentResult {
|
|
403
|
+
/** The child's final summary text (50 KiB cap, see spawnSubagent). */
|
|
404
|
+
text: string;
|
|
405
|
+
/** The child's own session id, so the parent can reference/inspect the transcript. */
|
|
406
|
+
sessionId: string;
|
|
407
|
+
truncated: boolean;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Runs one child agent loop for a single `task` call and returns its final
|
|
411
|
+
* summary. Isolation is structural: a fresh permission engine per spawn (the
|
|
412
|
+
* parent's remembered grants never inherit; deny rules still win first), a
|
|
413
|
+
* fresh session store (the child transcript is its own JSONL file), and a
|
|
414
|
+
* task-free toolset. Child asks surface through the parent's sink, each one a
|
|
415
|
+
* fresh decision. `ctx.signal` propagates: on abort the child loop stops and
|
|
416
|
+
* the error surfaces as the parent's tool result.
|
|
417
|
+
*/
|
|
418
|
+
export declare function spawnSubagent(input: {
|
|
419
|
+
prompt: string;
|
|
420
|
+
}, deps: SubagentDeps, ctx: ToolContext): Promise<SubagentResult>;
|
|
421
|
+
//#endregion
|
|
422
|
+
export type { AgentEvent, AssistantMessage, CompactConversationOptions, CompactSessionOptions, CompactionEntry, CompactionOptions, CompactionOutcome, CompactionResult, CreateSessionOptions, Message, ModelAdapter, ModelEvent, ModelProfile, ResumeSessionOptions, RunAgentOptions, SessionEntry, SessionHeader, SessionStore, StopReason, SubagentDeps, SubagentResult, SystemMessage, TextBlock, ToolCallBlock, ToolContext, ToolDefinition, ToolResultMessage, Usage, UserMessage };
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { appendFile, mkdir, readFile, readdir, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { createPermissionEngine, createRememberingEngine } from "@chantier/permissions";
|
|
5
6
|
//#region src/compaction.ts
|
|
6
7
|
/**
|
|
7
8
|
* Rough token estimate: ~4 characters per token for English/code text. Used
|
|
@@ -195,8 +196,11 @@ function sessionsDirFor(sessionsRoot, cwd) {
|
|
|
195
196
|
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 12);
|
|
196
197
|
return path.join(sessionsRoot, hash);
|
|
197
198
|
}
|
|
199
|
+
let sequence = 0;
|
|
198
200
|
function newSessionId() {
|
|
199
|
-
|
|
201
|
+
sequence = (sequence + 1) % 1679616;
|
|
202
|
+
const seq = sequence.toString(36).padStart(4, "0");
|
|
203
|
+
return `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${seq}${randomBytes(2).toString("hex")}`;
|
|
200
204
|
}
|
|
201
205
|
function buildStore(file, dir, id, selfId) {
|
|
202
206
|
const readEntries = async (loadId) => {
|
|
@@ -666,9 +670,10 @@ async function executeCall(call, opts, byName) {
|
|
|
666
670
|
}
|
|
667
671
|
//#endregion
|
|
668
672
|
//#region src/context.ts
|
|
669
|
-
/**
|
|
670
|
-
|
|
671
|
-
|
|
673
|
+
/**
|
|
674
|
+
* Tool-usage rules the model needs to drive the harness correctly.
|
|
675
|
+
*/
|
|
676
|
+
const TOOL_RULES = `# Tool usage rules
|
|
672
677
|
|
|
673
678
|
- Paths are relative to the project cwd unless you pass an absolute path deliberately.
|
|
674
679
|
- Protected paths (.env, .env.*, *.pem, id_rsa*, ~/.ssh) are refused by the harness; do not retry them.
|
|
@@ -680,17 +685,100 @@ const TOOL_RULES = `
|
|
|
680
685
|
const IDENTITY = `You are chantier, a terminal coding agent. You work inside the user's project directory:
|
|
681
686
|
read before you write, make surgical edits, and explain what you did in one short paragraph at the end
|
|
682
687
|
of a task. When a mutation is denied, state it plainly and continue with what is allowed.`;
|
|
683
|
-
/**
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
688
|
+
/** Doing-tasks discipline shared by every model family. */
|
|
689
|
+
const DOING_TASKS = `# Doing tasks
|
|
690
|
+
|
|
691
|
+
- Prefer editing existing files over creating new ones.
|
|
692
|
+
- Do exactly what was asked: no scope creep — no extra retries, telemetry, or
|
|
693
|
+
abstraction "while you're at it"; the real ask only.
|
|
694
|
+
- Comments explain WHY, not WHAT; skip them where the code already says it.
|
|
695
|
+
- Verify behavioral changes by running the changed path, not by re-reading the edit.
|
|
696
|
+
- State uncertainty plainly rather than guessing.`;
|
|
697
|
+
const DENIALS = `# Permission denials
|
|
689
698
|
|
|
690
|
-
|
|
699
|
+
A denied tool call is final for that exact invocation: never retry the identical
|
|
700
|
+
denied call. Adjust the arguments or switch the approach, or continue with what
|
|
701
|
+
is allowed, and state plainly that the action was not permitted.`;
|
|
702
|
+
/** Present in the prompt only when a tool named "task" is offered. */
|
|
703
|
+
const DELEGATION = `# Delegating subtasks
|
|
691
704
|
|
|
692
|
-
|
|
693
|
-
|
|
705
|
+
- Delegate self-contained subtasks with the full context the child needs in the
|
|
706
|
+
prompt (paths, constraints, acceptance); the child returns a final summary.
|
|
707
|
+
- Scale the prompt effort to the subtask: brief for mechanical work, detailed
|
|
708
|
+
for design work.
|
|
709
|
+
- Do not delegate single sequential edits you can do directly.`;
|
|
710
|
+
/** Default semantics: today's identity, unchanged. */
|
|
711
|
+
const DEFAULT_PROFILE = { name: "default" };
|
|
712
|
+
const GLM_PROFILE = {
|
|
713
|
+
name: "glm",
|
|
714
|
+
identity: `You are chantier, a terminal coding agent working directly in the user's project directory.
|
|
715
|
+
Style for this model family: keep prose terse; tool arguments are strict JSON
|
|
716
|
+
objects with no trailing commentary inside tool calls; finish each task with one
|
|
717
|
+
short final paragraph and nothing more. When a tool call is denied or fails,
|
|
718
|
+
adjust the arguments or change the approach — never loop identical retries.`
|
|
719
|
+
};
|
|
720
|
+
const CLAUDE_PROFILE = {
|
|
721
|
+
name: "claude",
|
|
722
|
+
identity: `You are chantier, a terminal coding agent working directly in the user's project directory.
|
|
723
|
+
Read before you write; strongly prefer editing existing files over creating new
|
|
724
|
+
ones; do exactly the task asked with no scope creep; when something is unclear,
|
|
725
|
+
say so plainly instead of guessing.`
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Profile registry: family prefix match on the model id — `glm-` and `claude-`
|
|
729
|
+
* resolve their profiles, anything else the default. Prefixes match the leading
|
|
730
|
+
* id so suffixed tags (e.g. `glm-5.3-flash:cloud`) still resolve to the family.
|
|
731
|
+
*/
|
|
732
|
+
function resolveModelProfile(model) {
|
|
733
|
+
if (model.startsWith("glm-")) return GLM_PROFILE;
|
|
734
|
+
if (model.startsWith("claude-")) return CLAUDE_PROFILE;
|
|
735
|
+
return DEFAULT_PROFILE;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Builds the system prompt from fixed, blank-line-joined sections: environment,
|
|
739
|
+
* identity (profile-adjustable), doing-tasks rules, denial rule, delegation
|
|
740
|
+
* (only when a `task` tool is offered), tool catalog, tool rules, and every
|
|
741
|
+
* AGENTS.md from cwd up to the git root last.
|
|
742
|
+
*
|
|
743
|
+
* `profile` omitted means the default profile: identity and tool rules keep
|
|
744
|
+
* today's semantics, new sections are purely additive.
|
|
745
|
+
*/
|
|
746
|
+
async function buildSystemPrompt(cwd, tools, profile = DEFAULT_PROFILE) {
|
|
747
|
+
const toolCatalog = tools.map((tool) => `- ${tool.name}${tool.readOnly ? " (read-only)" : ""}: ${tool.description.split(".")[0]}.`).join("\n");
|
|
748
|
+
const sections = [
|
|
749
|
+
await environmentSection(cwd),
|
|
750
|
+
profile.identity ?? IDENTITY,
|
|
751
|
+
DOING_TASKS,
|
|
752
|
+
DENIALS
|
|
753
|
+
];
|
|
754
|
+
if (tools.some((tool) => tool.name === "task")) sections.push(DELEGATION);
|
|
755
|
+
sections.push(`# Available tools\n\n${toolCatalog}`, TOOL_RULES);
|
|
756
|
+
const agentsDocs = await collectAgentsMd(cwd);
|
|
757
|
+
if (agentsDocs.length > 0) sections.push(`# Project instructions (AGENTS.md)\n\n${agentsDocs.join("\n\n")}`);
|
|
758
|
+
return sections.join("\n\n");
|
|
759
|
+
}
|
|
760
|
+
/** Environment facts first; the branch line is omitted rather than fabricated. */
|
|
761
|
+
async function environmentSection(cwd) {
|
|
762
|
+
const branch = await resolveBranch(await findGitRoot(path.resolve(cwd)));
|
|
763
|
+
const lines = [
|
|
764
|
+
"# Environment",
|
|
765
|
+
"",
|
|
766
|
+
`- cwd: ${cwd}`,
|
|
767
|
+
`- date: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} (UTC)`
|
|
768
|
+
];
|
|
769
|
+
if (branch !== null) lines.push(`- branch: ${branch}`);
|
|
770
|
+
return lines.join("\n");
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Branch read from the filesystem, no git process spawned: `.git/HEAD` with
|
|
774
|
+
* `ref: refs/heads/<name>` yields the name; a raw sha means detached. Missing
|
|
775
|
+
* or unreadable HEAD → null → the caller omits the branch line.
|
|
776
|
+
*/
|
|
777
|
+
async function resolveBranch(gitRoot) {
|
|
778
|
+
if (gitRoot === null) return null;
|
|
779
|
+
const head = await readFile(path.join(gitRoot, ".git", "HEAD"), "utf8").catch(() => null);
|
|
780
|
+
if (head === null) return null;
|
|
781
|
+
return /^ref: refs\/heads\/(.+)$/.exec(head.trim())?.[1] ?? "(detached)";
|
|
694
782
|
}
|
|
695
783
|
/**
|
|
696
784
|
* AGENTS.md discovery: walk from cwd to the git root. Files from ancestor
|
|
@@ -726,4 +814,70 @@ async function findGitRoot(dir) {
|
|
|
726
814
|
}
|
|
727
815
|
}
|
|
728
816
|
//#endregion
|
|
729
|
-
|
|
817
|
+
//#region src/subagent.ts
|
|
818
|
+
/** The returned summary is capped at 50 KiB; the full transcript stays in the child session. */
|
|
819
|
+
const SUMMARY_CAP_CHARS = 51200;
|
|
820
|
+
/** Children get their own (smaller) turn budget; the parent's cap says nothing about subtasks. */
|
|
821
|
+
const DEFAULT_SUBAGENT_TURNS = 25;
|
|
822
|
+
/**
|
|
823
|
+
* Plain-string appendix for the child system prompt. Kept out of the composer
|
|
824
|
+
* on purpose: the subagent role is a property of delegation, not of the
|
|
825
|
+
* project's base prompt.
|
|
826
|
+
*/
|
|
827
|
+
const SUBAGENT_ROLE_APPENDIX = "\n\n# Subagent role\n\nYou are a subagent spawned by the parent agent to complete one self-contained task. Work within the prompt you were given, use the allowed tools, and end with a single short summary paragraph of what you did and found. Do not ask the user questions; decide and act.";
|
|
828
|
+
/**
|
|
829
|
+
* Runs one child agent loop for a single `task` call and returns its final
|
|
830
|
+
* summary. Isolation is structural: a fresh permission engine per spawn (the
|
|
831
|
+
* parent's remembered grants never inherit; deny rules still win first), a
|
|
832
|
+
* fresh session store (the child transcript is its own JSONL file), and a
|
|
833
|
+
* task-free toolset. Child asks surface through the parent's sink, each one a
|
|
834
|
+
* fresh decision. `ctx.signal` propagates: on abort the child loop stops and
|
|
835
|
+
* the error surfaces as the parent's tool result.
|
|
836
|
+
*/
|
|
837
|
+
async function spawnSubagent(input, deps, ctx) {
|
|
838
|
+
const childPermission = createRememberingEngine(createPermissionEngine(deps.rules));
|
|
839
|
+
const childSession = await createSessionStore({
|
|
840
|
+
cwd: ctx.cwd,
|
|
841
|
+
provider: deps.provider,
|
|
842
|
+
model: deps.model
|
|
843
|
+
});
|
|
844
|
+
const system = `${await buildSystemPrompt(ctx.cwd, deps.tools)}${SUBAGENT_ROLE_APPENDIX}`;
|
|
845
|
+
const userMessage = {
|
|
846
|
+
role: "user",
|
|
847
|
+
content: [{
|
|
848
|
+
type: "text",
|
|
849
|
+
text: input.prompt
|
|
850
|
+
}]
|
|
851
|
+
};
|
|
852
|
+
await childSession.append({
|
|
853
|
+
type: "message",
|
|
854
|
+
message: userMessage
|
|
855
|
+
});
|
|
856
|
+
let text = "";
|
|
857
|
+
for await (const event of runAgent({
|
|
858
|
+
adapter: deps.adapter,
|
|
859
|
+
tools: deps.tools,
|
|
860
|
+
permission: childPermission,
|
|
861
|
+
sink: deps.sink,
|
|
862
|
+
session: childSession,
|
|
863
|
+
cwd: ctx.cwd,
|
|
864
|
+
system,
|
|
865
|
+
messages: [userMessage],
|
|
866
|
+
maxTurns: deps.maxTurns ?? DEFAULT_SUBAGENT_TURNS,
|
|
867
|
+
contextWindow: deps.contextWindow,
|
|
868
|
+
compaction: deps.contextWindow === void 0 ? void 0 : { enabled: true },
|
|
869
|
+
signal: ctx.signal
|
|
870
|
+
})) if (event.type === "result") text = event.text;
|
|
871
|
+
if (text.length <= SUMMARY_CAP_CHARS) return {
|
|
872
|
+
text,
|
|
873
|
+
sessionId: childSession.id,
|
|
874
|
+
truncated: false
|
|
875
|
+
};
|
|
876
|
+
return {
|
|
877
|
+
text: `${text.slice(0, SUMMARY_CAP_CHARS)}\n[truncated: subagent summary exceeded 50 KiB cap; full transcript session id: ${childSession.id}]`,
|
|
878
|
+
sessionId: childSession.id,
|
|
879
|
+
truncated: true
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
//#endregion
|
|
883
|
+
export { COMPACTED_MARKER, COMPACT_PROMPT, DEFAULT_COMPACTION_KEEP_RECENT, DEFAULT_COMPACTION_RESERVE, DEFAULT_SESSIONS_ROOT, alignedMessageOrdinals, buildSystemPrompt, compactConversation, compactSession, compactedSummaryMessage, createSessionStore, estimateMessageTokens, estimateTokens, loadNewestSessionId, looksLikeContextOverflow, resolveModelProfile, resumeSessionStore, runAgent, serializeConversation, sessionView, sessionsDirFor, shouldCompact, spawnSubagent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chantier/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Agent loop, session store, and model-adapter seam for the chantier coding agent",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"prepublishOnly": "npm run build"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@chantier/permissions": "^0.
|
|
39
|
+
"@chantier/permissions": "^0.5.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"tsdown": "0.23.0"
|