@gajae-code/coding-agent 0.8.2 → 0.9.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/CHANGELOG.md +29 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +8 -0
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +7 -7
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/settings-schema.ts +68 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +164 -15
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1593 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +14 -5
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +9 -1
- package/src/modes/rpc/rpc-mode.ts +29 -3
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config.ts +16 -4
- package/src/notifications/index.ts +78 -5
- package/src/notifications/telegram-daemon-cli.ts +2 -2
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +85 -8
- package/src/notifications/threaded-render.ts +17 -1
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +1 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +32 -22
- package/src/session/agent-session.ts +732 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +65 -0
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/web/scrapers/youtube.ts +24 -12
package/src/system-prompt.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { loadSkills, type Skill } from "./extensibility/skills";
|
|
|
14
14
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
15
15
|
import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
|
|
16
16
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
17
|
+
import volatileProjectContextTemplate from "./prompts/system/volatile-project-context.md" with { type: "text" };
|
|
17
18
|
import { shortenPath } from "./tools/render-utils";
|
|
18
19
|
import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
|
|
19
20
|
|
|
@@ -242,9 +243,12 @@ function dedupeExactContextFiles(
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
/**
|
|
245
|
-
* Load all
|
|
246
|
+
* Load all context files using the capability API.
|
|
246
247
|
* Returns {path, content, depth} entries for all discovered context files.
|
|
247
|
-
*
|
|
248
|
+
* Native user-global files (`~/.gjc/agent/AGENTS.md`) come first, then project
|
|
249
|
+
* files sorted by depth (descending) so files closer to cwd appear last/more
|
|
250
|
+
* prominent. User-home files from foreign providers (`~/.claude/CLAUDE.md`,
|
|
251
|
+
* `~/.codex/AGENTS.md`, …) stay excluded — only gjc's own user config applies.
|
|
248
252
|
*/
|
|
249
253
|
export async function loadProjectContextFiles(
|
|
250
254
|
options: LoadContextFilesOptions = {},
|
|
@@ -252,28 +256,32 @@ export async function loadProjectContextFiles(
|
|
|
252
256
|
const resolvedCwd = options.cwd ?? getProjectDir();
|
|
253
257
|
|
|
254
258
|
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
259
|
+
const items = result.items as ContextFile[];
|
|
260
|
+
|
|
261
|
+
// Native user-global context applies everywhere and is least specific, so it
|
|
262
|
+
// renders first — project files rendered later take precedence over it.
|
|
263
|
+
const userFiles = items
|
|
264
|
+
.filter(item => item.level === "user" && item._source.provider === "native")
|
|
265
|
+
.map(item => ({ path: item.path, content: item.content }));
|
|
255
266
|
|
|
256
267
|
// Convert project-level ContextFile items and preserve depth info
|
|
257
|
-
const
|
|
258
|
-
.filter(item =>
|
|
259
|
-
.map(item => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
depth: contextFile.depth,
|
|
265
|
-
};
|
|
266
|
-
});
|
|
268
|
+
const projectFiles = items
|
|
269
|
+
.filter(item => item.level === "project")
|
|
270
|
+
.map(item => ({
|
|
271
|
+
path: item.path,
|
|
272
|
+
content: item.content,
|
|
273
|
+
depth: item.depth,
|
|
274
|
+
}));
|
|
267
275
|
|
|
268
276
|
// Sort by depth (descending): higher depth (farther from cwd) comes first,
|
|
269
277
|
// so files closer to cwd appear later and are more prominent
|
|
270
|
-
|
|
278
|
+
projectFiles.sort((a, b) => {
|
|
271
279
|
const depthA = a.depth ?? -1;
|
|
272
280
|
const depthB = b.depth ?? -1;
|
|
273
281
|
return depthB - depthA;
|
|
274
282
|
});
|
|
275
283
|
|
|
276
|
-
return dedupeExactContextFiles(
|
|
284
|
+
return dedupeExactContextFiles([...userFiles, ...projectFiles]);
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
/**
|
|
@@ -368,6 +376,13 @@ export interface BuildSystemPromptOptions {
|
|
|
368
376
|
secretsEnabled?: boolean;
|
|
369
377
|
/** Pre-loaded workspace tree (skips discovery if provided). May be a Promise to allow early kick-off. */
|
|
370
378
|
workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
|
|
379
|
+
/**
|
|
380
|
+
* Render a trimmed role-agent base prompt for subagent sessions: omits the
|
|
381
|
+
* workflow-surface/routing/self-awareness (`<gjc-runtime>`) and `<soul>` blocks
|
|
382
|
+
* that only apply to the top-level interactive/print agent. Tool safety, repo
|
|
383
|
+
* safety, and the completion contract are retained. Default: false.
|
|
384
|
+
*/
|
|
385
|
+
subagent?: boolean;
|
|
371
386
|
}
|
|
372
387
|
|
|
373
388
|
/** Result of building provider-facing system prompt messages. */
|
|
@@ -375,6 +390,29 @@ export interface BuildSystemPromptResult {
|
|
|
375
390
|
/** Ordered system prompt blocks. Providers should preserve entries as distinct messages/blocks. */
|
|
376
391
|
systemPrompt: string[];
|
|
377
392
|
}
|
|
393
|
+
export interface BuildVolatileProjectContextOptions {
|
|
394
|
+
cwd?: string;
|
|
395
|
+
date?: string;
|
|
396
|
+
workspaceTree?: WorkspaceTree;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function buildVolatileProjectContext(options: BuildVolatileProjectContextOptions = {}): string {
|
|
400
|
+
const resolvedCwd = options.cwd ?? getProjectDir();
|
|
401
|
+
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
402
|
+
return prompt
|
|
403
|
+
.render(volatileProjectContextTemplate, {
|
|
404
|
+
date,
|
|
405
|
+
cwd: shortenPath(resolvedCwd.replace(/\\/g, "/")),
|
|
406
|
+
workspaceTree: options.workspaceTree ?? {
|
|
407
|
+
rootPath: resolvedCwd,
|
|
408
|
+
rendered: "",
|
|
409
|
+
truncated: false,
|
|
410
|
+
totalLines: 0,
|
|
411
|
+
agentsMdFiles: [],
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
.trim();
|
|
415
|
+
}
|
|
378
416
|
|
|
379
417
|
/** Build the system prompt with tools, guidelines, and context */
|
|
380
418
|
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<BuildSystemPromptResult> {
|
|
@@ -403,6 +441,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
403
441
|
eagerTasks = false,
|
|
404
442
|
secretsEnabled = false,
|
|
405
443
|
workspaceTree: providedWorkspaceTree,
|
|
444
|
+
subagent = false,
|
|
406
445
|
} = options;
|
|
407
446
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
408
447
|
|
|
@@ -534,6 +573,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
534
573
|
// Build tool descriptions for system prompt rendering.
|
|
535
574
|
const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
|
|
536
575
|
const toolRefs = Object.fromEntries(toolPromptNames.entries());
|
|
576
|
+
const hasHiddenToolDiscoveryTool = Object.hasOwn(toolRefs, "search_tool_bm25");
|
|
537
577
|
const toolInfo = toolNames.map(name => ({
|
|
538
578
|
name: toolPromptNames.get(name) ?? name,
|
|
539
579
|
internalName: name,
|
|
@@ -575,13 +615,14 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
575
615
|
cwd: promptCwd,
|
|
576
616
|
intentTracing: !!intentField,
|
|
577
617
|
intentField: intentField ?? "",
|
|
578
|
-
mcpDiscoveryMode,
|
|
618
|
+
mcpDiscoveryMode: mcpDiscoveryMode && hasHiddenToolDiscoveryTool,
|
|
579
619
|
hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
|
|
580
620
|
mcpDiscoveryServerSummaries,
|
|
581
|
-
toolDiscoveryActive,
|
|
621
|
+
toolDiscoveryActive: toolDiscoveryActive && hasHiddenToolDiscoveryTool,
|
|
582
622
|
discoverableTools,
|
|
583
623
|
eagerTasks,
|
|
584
624
|
secretsEnabled,
|
|
625
|
+
subagent,
|
|
585
626
|
};
|
|
586
627
|
const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
587
628
|
const systemPrompt = [rendered];
|
package/src/task/executor.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { runExtensionCompact, runExtensionSetModel } from "../extensibility/exte
|
|
|
22
22
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
23
23
|
import { buildAgentSubskillInjection, renderAgentPromptAdditions } from "../extensibility/gjc-plugins";
|
|
24
24
|
import { buildSkillPromptMessage, type Skill } from "../extensibility/skills";
|
|
25
|
+
import { sessionRoot } from "../gjc-runtime/session-layout";
|
|
25
26
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
26
27
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
27
28
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
@@ -42,6 +43,7 @@ import type { EventBus } from "../utils/event-bus";
|
|
|
42
43
|
import { buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
43
44
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
44
45
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
46
|
+
import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
|
|
45
47
|
import {
|
|
46
48
|
type AgentDefinition,
|
|
47
49
|
type AgentProgress,
|
|
@@ -245,6 +247,51 @@ function previewOffendingData(value: unknown, maxLength = 500): string {
|
|
|
245
247
|
}
|
|
246
248
|
return serialized.length > maxLength ? `${serialized.slice(0, maxLength)}…` : serialized;
|
|
247
249
|
}
|
|
250
|
+
const PLACEHOLDER_YIELD_PATTERNS = [
|
|
251
|
+
/^see (?:the )?message body(?:\b|[\s—:.,-])/i,
|
|
252
|
+
/^(?:complete\s+\w+\s+)?returned inline(?:\b|[\s—:.,-])/i,
|
|
253
|
+
/^leader persists(?:\b|[\s—:.,-])/i,
|
|
254
|
+
/^caller persists(?:\b|[\s—:.,-])/i,
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
const PLACEHOLDER_YIELD_FIELD_NAMES = new Set([
|
|
258
|
+
"artifactmarkdown",
|
|
259
|
+
"finalmarkdown",
|
|
260
|
+
"fullplan",
|
|
261
|
+
"markdown",
|
|
262
|
+
"planmarkdown",
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
function looksLikePlaceholderYieldString(value: string): boolean {
|
|
266
|
+
const trimmed = value.trim();
|
|
267
|
+
if (trimmed.length === 0 || trimmed.length > 500) return false;
|
|
268
|
+
return PLACEHOLDER_YIELD_PATTERNS.some(pattern => pattern.test(trimmed));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizePlaceholderYieldFieldName(key: string): string {
|
|
272
|
+
return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function findPlaceholderYieldPath(value: unknown, path = "$", depth = 0, inspectStrings = true): string | undefined {
|
|
276
|
+
if (typeof value === "string") {
|
|
277
|
+
return inspectStrings && looksLikePlaceholderYieldString(value) ? path : undefined;
|
|
278
|
+
}
|
|
279
|
+
if (!value || typeof value !== "object" || depth > 4) return undefined;
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
for (let i = 0; i < value.length; i++) {
|
|
282
|
+
const found = findPlaceholderYieldPath(value[i], `${path}[${i}]`, depth + 1, false);
|
|
283
|
+
if (found) return found;
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const record = value as Record<string, unknown>;
|
|
288
|
+
for (const [key, item] of Object.entries(record)) {
|
|
289
|
+
const shouldInspectString = PLACEHOLDER_YIELD_FIELD_NAMES.has(normalizePlaceholderYieldFieldName(key));
|
|
290
|
+
const found = findPlaceholderYieldPath(item, `${path}.${key}`, depth + 1, shouldInspectString);
|
|
291
|
+
if (found) return found;
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
248
295
|
|
|
249
296
|
function tryParseJsonOutput(text: string): unknown | undefined {
|
|
250
297
|
const trimmed = text.trim();
|
|
@@ -321,6 +368,8 @@ interface FinalizeSubprocessOutputResult {
|
|
|
321
368
|
export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
|
|
322
369
|
export const SUBAGENT_WARNING_MISSING_YIELD =
|
|
323
370
|
"SYSTEM WARNING: Subagent exited without calling yield tool after 3 reminders.";
|
|
371
|
+
export const SUBAGENT_WARNING_PLACEHOLDER_YIELD =
|
|
372
|
+
"SYSTEM WARNING: Subagent yield data contains a placeholder instead of the actual result.";
|
|
324
373
|
|
|
325
374
|
/** Build a schema_violation outcome — surfaced as a non-zero exit so callers treat it as a failure. */
|
|
326
375
|
function buildSchemaViolationOutcome(
|
|
@@ -347,6 +396,21 @@ function buildSchemaViolationOutcome(
|
|
|
347
396
|
return { rawOutput, stderr: headline, exitCode: 1 };
|
|
348
397
|
}
|
|
349
398
|
|
|
399
|
+
function buildPlaceholderYieldOutcome(
|
|
400
|
+
placeholderPath: string,
|
|
401
|
+
data: unknown,
|
|
402
|
+
): { rawOutput: string; stderr: string; exitCode: number } {
|
|
403
|
+
return buildSchemaViolationOutcome(
|
|
404
|
+
{
|
|
405
|
+
message:
|
|
406
|
+
`${SUBAGENT_WARNING_PLACEHOLDER_YIELD} Offending path: ${placeholderPath}. ` +
|
|
407
|
+
"Return the real payload in yield.result.data or persist a durable artifact receipt.",
|
|
408
|
+
missingRequired: [],
|
|
409
|
+
},
|
|
410
|
+
data,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
350
414
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
351
415
|
let { rawOutput, exitCode, stderr } = args;
|
|
352
416
|
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
@@ -376,21 +440,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
376
440
|
stderr = `schema_violation: invalid output schema: ${schemaError}`;
|
|
377
441
|
exitCode = 1;
|
|
378
442
|
} else {
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
const outcome =
|
|
443
|
+
const placeholderPath = findPlaceholderYieldPath(completeData);
|
|
444
|
+
if (placeholderPath) {
|
|
445
|
+
const outcome = buildPlaceholderYieldOutcome(placeholderPath, completeData);
|
|
382
446
|
rawOutput = outcome.rawOutput;
|
|
383
447
|
stderr = outcome.stderr;
|
|
384
448
|
exitCode = outcome.exitCode;
|
|
385
449
|
} else {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
450
|
+
const verdict = validator ? validator.validate(completeData) : { ok: true as const };
|
|
451
|
+
if (!verdict.ok) {
|
|
452
|
+
const outcome = buildSchemaViolationOutcome(verdict, completeData);
|
|
453
|
+
rawOutput = outcome.rawOutput;
|
|
454
|
+
stderr = outcome.stderr;
|
|
455
|
+
exitCode = outcome.exitCode;
|
|
456
|
+
} else {
|
|
457
|
+
try {
|
|
458
|
+
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
461
|
+
rawOutput = `{"error":"Failed to serialize yield data: ${errorMessage}"}`;
|
|
462
|
+
}
|
|
463
|
+
exitCode = 0;
|
|
464
|
+
stderr = "";
|
|
391
465
|
}
|
|
392
|
-
exitCode = 0;
|
|
393
|
-
stderr = "";
|
|
394
466
|
}
|
|
395
467
|
}
|
|
396
468
|
}
|
|
@@ -403,21 +475,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
403
475
|
if (fallback) {
|
|
404
476
|
const completeData = normalizeCompleteData(fallback.data, reportFindings);
|
|
405
477
|
const { validator } = buildOutputValidator(outputSchema);
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
const outcome =
|
|
478
|
+
const placeholderPath = findPlaceholderYieldPath(completeData);
|
|
479
|
+
if (placeholderPath) {
|
|
480
|
+
const outcome = buildPlaceholderYieldOutcome(placeholderPath, completeData);
|
|
409
481
|
rawOutput = outcome.rawOutput;
|
|
410
482
|
stderr = outcome.stderr;
|
|
411
483
|
exitCode = outcome.exitCode;
|
|
412
484
|
} else {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
485
|
+
const verdict = validator ? validator.validate(completeData) : { ok: true as const };
|
|
486
|
+
if (!verdict.ok) {
|
|
487
|
+
const outcome = buildSchemaViolationOutcome(verdict, completeData);
|
|
488
|
+
rawOutput = outcome.rawOutput;
|
|
489
|
+
stderr = outcome.stderr;
|
|
490
|
+
exitCode = outcome.exitCode;
|
|
491
|
+
} else {
|
|
492
|
+
try {
|
|
493
|
+
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
494
|
+
} catch (err) {
|
|
495
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
496
|
+
rawOutput = `{"error":"Failed to serialize fallback completion: ${errorMessage}"}`;
|
|
497
|
+
}
|
|
498
|
+
exitCode = 0;
|
|
499
|
+
stderr = "";
|
|
418
500
|
}
|
|
419
|
-
exitCode = 0;
|
|
420
|
-
stderr = "";
|
|
421
501
|
}
|
|
422
502
|
} else if (!hasOutputSchema && allowFallback && rawOutput.trim().length > 0) {
|
|
423
503
|
exitCode = 0;
|
|
@@ -1209,6 +1289,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1209
1289
|
const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
|
|
1210
1290
|
? { id, name: agent.name, description: agent.description }
|
|
1211
1291
|
: undefined;
|
|
1292
|
+
let subagentTokenTurn = 0;
|
|
1293
|
+
const tokenLogDir = options.parentSessionId
|
|
1294
|
+
? path.join(sessionRoot(cwd, options.parentSessionId), "token-logs")
|
|
1295
|
+
: undefined;
|
|
1212
1296
|
const subagentTelemetry: AgentTelemetryConfig | undefined =
|
|
1213
1297
|
options.parentTelemetry && subagentAgentIdentity
|
|
1214
1298
|
? {
|
|
@@ -1217,6 +1301,27 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1217
1301
|
// Clear parent's conversationId; the child loop falls back to
|
|
1218
1302
|
// its own AgentLoopConfig.sessionId.
|
|
1219
1303
|
conversationId: undefined,
|
|
1304
|
+
// Intentionally REPLACES (does not chain) the parent's onChatUsage:
|
|
1305
|
+
// the parent handler attributes turns to subagentId "root", so
|
|
1306
|
+
// chaining it here would double-log every subagent turn under root.
|
|
1307
|
+
// Subagent turns are attributed to this child's id instead.
|
|
1308
|
+
onChatUsage: async event => {
|
|
1309
|
+
if (!tokenLogDir) return;
|
|
1310
|
+
subagentTokenTurn += 1;
|
|
1311
|
+
await persistTaskTokenLog(
|
|
1312
|
+
taskTokenLogFromUsage(event.usage, {
|
|
1313
|
+
subagentId: id,
|
|
1314
|
+
agent: agent.name,
|
|
1315
|
+
// Monotonic 1-based sequence per subagent session
|
|
1316
|
+
// (event.stepNumber is 0-based and -1 for oneshots).
|
|
1317
|
+
turn: subagentTokenTurn,
|
|
1318
|
+
at: new Date().toISOString(),
|
|
1319
|
+
model: event.model,
|
|
1320
|
+
cost: event.cost,
|
|
1321
|
+
}),
|
|
1322
|
+
{ dir: tokenLogDir },
|
|
1323
|
+
);
|
|
1324
|
+
},
|
|
1220
1325
|
}
|
|
1221
1326
|
: undefined;
|
|
1222
1327
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
|
|
1
2
|
import type { ForkContextMode } from "./types";
|
|
2
3
|
|
|
3
4
|
export interface ForkContextAdvisory {
|
|
@@ -12,13 +13,7 @@ export interface ForkContextAdvisory {
|
|
|
12
13
|
* many parent-context tokens each mode may clone into the child; the actual
|
|
13
14
|
* cloned amount can never exceed the parent context itself.
|
|
14
15
|
*/
|
|
15
|
-
const CLONE_BUDGET_BY_MODE =
|
|
16
|
-
none: 0,
|
|
17
|
-
receipt: 2000,
|
|
18
|
-
"last-turn": 4000,
|
|
19
|
-
bounded: 8000,
|
|
20
|
-
full: 15000,
|
|
21
|
-
} as const satisfies Record<ForkContextMode, number>;
|
|
16
|
+
const CLONE_BUDGET_BY_MODE = FORK_CONTEXT_TOKEN_BUDGET_BY_MODE;
|
|
22
17
|
|
|
23
18
|
const RECEIPT_TRIGGERS = [
|
|
24
19
|
{ pattern: /as discussed/i, reason: "prior-session-reference:as-discussed" },
|
package/src/task/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import * as git from "../utils/git";
|
|
|
48
48
|
import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
|
|
49
49
|
import { runSubprocess } from "./executor";
|
|
50
50
|
import { adviseForkContextMode } from "./fork-context-advisory";
|
|
51
|
+
import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
|
|
51
52
|
import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
|
|
52
53
|
import { AgentOutputManager } from "./output-manager";
|
|
53
54
|
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
@@ -303,11 +304,11 @@ function resolveForkSeedParamsForMode(
|
|
|
303
304
|
case "none":
|
|
304
305
|
return undefined;
|
|
305
306
|
case "receipt":
|
|
306
|
-
return { maxMessages: 1, maxTokens:
|
|
307
|
+
return { maxMessages: 1, maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE.receipt };
|
|
307
308
|
case "last-turn":
|
|
308
|
-
return { maxMessages: 2, maxTokens:
|
|
309
|
+
return { maxMessages: 2, maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE["last-turn"] };
|
|
309
310
|
case "bounded":
|
|
310
|
-
return { maxMessages: capMessages(50), maxTokens:
|
|
311
|
+
return { maxMessages: capMessages(50), maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE.bounded };
|
|
311
312
|
case "full":
|
|
312
313
|
return { maxMessages: capMessages(500), maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, model) };
|
|
313
314
|
default:
|
|
@@ -14,6 +14,8 @@ function escapeRegExp(value: string): string {
|
|
|
14
14
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const RESERVED_OUTPUT_EXTENSIONS_PATTERN = "(?:md|jsonl|patch)";
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* Manages agent output ID allocation to ensure uniqueness.
|
|
19
21
|
*
|
|
@@ -62,8 +64,8 @@ export class AgentOutputManager {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
const pattern = this.#parentPrefix
|
|
65
|
-
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)
|
|
66
|
-
:
|
|
67
|
+
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`)
|
|
68
|
+
: new RegExp(`^(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`);
|
|
67
69
|
|
|
68
70
|
let maxId = -1;
|
|
69
71
|
for (const file of files) {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { GJC_SESSION_ACTIVITY_FILE, sessionRoot } from "../gjc-runtime/session-layout";
|
|
6
|
+
import {
|
|
7
|
+
computeCacheHitRate,
|
|
8
|
+
persistTaskTokenLog,
|
|
9
|
+
readTaskTokenLogs,
|
|
10
|
+
resolveTaskTokenLogDir,
|
|
11
|
+
taskTokenLogFromChat,
|
|
12
|
+
taskTokenLogFromUsage,
|
|
13
|
+
} from "./token-log";
|
|
14
|
+
|
|
15
|
+
async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
|
16
|
+
const dir = await mkdtemp(join(tmpdir(), "gjc-token-log-"));
|
|
17
|
+
try {
|
|
18
|
+
return await fn(dir);
|
|
19
|
+
} finally {
|
|
20
|
+
await rm(dir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("task token log", () => {
|
|
25
|
+
it("maps raw chat buckets without folding cache into input", () => {
|
|
26
|
+
const log = taskTokenLogFromChat(
|
|
27
|
+
{
|
|
28
|
+
inputTokens: 100,
|
|
29
|
+
outputTokens: 20,
|
|
30
|
+
cachedInputTokens: 30,
|
|
31
|
+
cacheWriteTokens: 5,
|
|
32
|
+
costUsd: 0.25,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
subagentId: "root",
|
|
36
|
+
agent: "main",
|
|
37
|
+
turn: 1,
|
|
38
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
39
|
+
model: "model-a",
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(log.input).toBe(100);
|
|
44
|
+
expect(log.output).toBe(20);
|
|
45
|
+
expect(log.cacheRead).toBe(30);
|
|
46
|
+
expect(log.cacheWrite).toBe(5);
|
|
47
|
+
expect(log.totalTokens).toBe(155);
|
|
48
|
+
expect(log.cost).toBe(0.25);
|
|
49
|
+
expect(log.model).toBe("model-a");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("maps zero-cache chat buckets without inventing cache tokens", () => {
|
|
53
|
+
const log = taskTokenLogFromChat(
|
|
54
|
+
{ inputTokens: 7, outputTokens: 3, cachedInputTokens: 0, cacheWriteTokens: 0 },
|
|
55
|
+
{ subagentId: "root", turn: 2, at: "2026-01-01T00:00:02.000Z" },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(log.input).toBe(7);
|
|
59
|
+
expect(log.output).toBe(3);
|
|
60
|
+
expect(log.cacheRead).toBe(0);
|
|
61
|
+
expect(log.cacheWrite).toBe(0);
|
|
62
|
+
expect(log.totalTokens).toBe(10);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("keeps large cache-only reads out of input and hits the cache-rate upper boundary", () => {
|
|
66
|
+
const log = taskTokenLogFromChat(
|
|
67
|
+
{ inputTokens: 0, outputTokens: 2, cachedInputTokens: 1_000_000_000, cacheWriteTokens: 0 },
|
|
68
|
+
{ subagentId: "root", turn: 3, at: "2026-01-01T00:00:03.000Z" },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(log.input).toBe(0);
|
|
72
|
+
expect(log.output).toBe(2);
|
|
73
|
+
expect(log.cacheRead).toBe(1_000_000_000);
|
|
74
|
+
expect(log.cacheWrite).toBe(0);
|
|
75
|
+
expect(log.totalTokens).toBe(1_000_000_002);
|
|
76
|
+
expect(computeCacheHitRate(log.input, log.cacheRead)).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("persists and reads a jsonl round trip", async () => {
|
|
80
|
+
await withTempDir(async dir => {
|
|
81
|
+
const first = taskTokenLogFromChat(
|
|
82
|
+
{ inputTokens: 1, outputTokens: 2, cachedInputTokens: 3, cacheWriteTokens: 4 },
|
|
83
|
+
{ subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
|
|
84
|
+
);
|
|
85
|
+
const second = taskTokenLogFromChat(
|
|
86
|
+
{ inputTokens: 5, outputTokens: 6, cachedInputTokens: 7, cacheWriteTokens: 8 },
|
|
87
|
+
{ subagentId: "child", agent: "executor", turn: 1, at: "2026-01-01T00:00:01.000Z" },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await persistTaskTokenLog(first, { dir });
|
|
91
|
+
await persistTaskTokenLog(second, { dir });
|
|
92
|
+
|
|
93
|
+
expect(await readTaskTokenLogs(dir)).toEqual([first, second]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("prefers the current session manager over a stale latest-active session", async () => {
|
|
98
|
+
await withTempDir(async cwd => {
|
|
99
|
+
const currentSessionId = "current-session";
|
|
100
|
+
const staleSessionId = "stale-session";
|
|
101
|
+
for (const [sessionId, updatedAt] of [
|
|
102
|
+
[currentSessionId, "2026-01-01T00:00:00.000Z"],
|
|
103
|
+
[staleSessionId, "2026-01-01T00:00:10.000Z"],
|
|
104
|
+
] as const) {
|
|
105
|
+
const markerPath = join(sessionRoot(cwd, sessionId), GJC_SESSION_ACTIVITY_FILE);
|
|
106
|
+
await mkdir(join(markerPath, ".."), { recursive: true });
|
|
107
|
+
await writeFile(
|
|
108
|
+
markerPath,
|
|
109
|
+
`${JSON.stringify({ session_id: sessionId, updated_at: updatedAt })}\n`,
|
|
110
|
+
"utf-8",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await expect(resolveTaskTokenLogDir(cwd, undefined, "")).resolves.toBe(
|
|
115
|
+
join(sessionRoot(cwd, staleSessionId), "token-logs"),
|
|
116
|
+
);
|
|
117
|
+
await expect(resolveTaskTokenLogDir(cwd, { getSessionId: () => currentSessionId }, "")).resolves.toBe(
|
|
118
|
+
join(sessionRoot(cwd, currentSessionId), "token-logs"),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("missing token-log file reads as empty", async () => {
|
|
124
|
+
await withTempDir(async dir => {
|
|
125
|
+
expect(await readTaskTokenLogs(dir)).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("subtracts cache from the aggregate ChatUsageSnapshot input bucket", () => {
|
|
130
|
+
// buildUsageSnapshot sets inputTokens = rawInput + cacheRead + cacheWrite.
|
|
131
|
+
// taskTokenLogFromUsage must recover the raw (cache-excluded) input.
|
|
132
|
+
const log = taskTokenLogFromUsage(
|
|
133
|
+
{
|
|
134
|
+
inputTokens: 135,
|
|
135
|
+
outputTokens: 20,
|
|
136
|
+
totalTokens: 155,
|
|
137
|
+
cachedInputTokens: 30,
|
|
138
|
+
cacheWriteTokens: 5,
|
|
139
|
+
reasoningOutputTokens: 0,
|
|
140
|
+
},
|
|
141
|
+
{ subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z", cost: { usd: 0.5 } },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(log.input).toBe(100);
|
|
145
|
+
expect(log.cacheRead).toBe(30);
|
|
146
|
+
expect(log.cacheWrite).toBe(5);
|
|
147
|
+
expect(log.output).toBe(20);
|
|
148
|
+
expect(log.totalTokens).toBe(155);
|
|
149
|
+
expect(log.cost).toBe(0.5);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("clamps aggregate input underflow to zero", () => {
|
|
153
|
+
const log = taskTokenLogFromUsage(
|
|
154
|
+
{
|
|
155
|
+
inputTokens: 10,
|
|
156
|
+
outputTokens: 1,
|
|
157
|
+
totalTokens: 10,
|
|
158
|
+
cachedInputTokens: 8,
|
|
159
|
+
cacheWriteTokens: 5,
|
|
160
|
+
reasoningOutputTokens: undefined,
|
|
161
|
+
},
|
|
162
|
+
{ subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(log.input).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("skips a torn or corrupt jsonl line instead of throwing", async () => {
|
|
169
|
+
await withTempDir(async dir => {
|
|
170
|
+
const valid = taskTokenLogFromChat(
|
|
171
|
+
{ inputTokens: 1, outputTokens: 2, cachedInputTokens: 3, cacheWriteTokens: 4 },
|
|
172
|
+
{ subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
|
|
173
|
+
);
|
|
174
|
+
await persistTaskTokenLog(valid, { dir });
|
|
175
|
+
// A partially-flushed / corrupt trailing line must not abort the read.
|
|
176
|
+
await appendFile(join(dir, "token-log.jsonl"), '{"subagentId":"root","turn":2,', "utf-8");
|
|
177
|
+
|
|
178
|
+
expect(await readTaskTokenLogs(dir)).toEqual([valid]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("throws when a non-empty log has no valid entries", async () => {
|
|
183
|
+
await withTempDir(async dir => {
|
|
184
|
+
await appendFile(join(dir, "token-log.jsonl"), "not json\n{}\n", "utf-8");
|
|
185
|
+
await expect(readTaskTokenLogs(dir)).rejects.toThrow(/corrupt token-log/);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|