@sayknow-cli/coding-agent 0.3.7 → 0.3.8
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/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -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/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +105 -1
- package/dist/types/config/settings.d.ts +8 -0
- 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/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- 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/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- 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 +14 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +23 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/notifications/threaded-render.d.ts +6 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +61 -6
- package/dist/types/session/agent-storage.d.ts +1 -0
- 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/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/skc-runtime/state-writer.d.ts +3 -0
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
- 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 +3 -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/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -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/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +8 -8
- 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/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- 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/notify-cli.ts +92 -8
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +68 -1
- package/src/config/settings.ts +22 -0
- package/src/defaults/skc/skills/team/SKILL.md +17 -14
- package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/skc-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 +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +14 -5
- package/src/main.ts +95 -43
- 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/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +181 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +66 -47
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +180 -18
- package/src/modes/controllers/selector-controller.ts +127 -24
- package/src/modes/interactive-mode.ts +51 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- 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/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config.ts +25 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +117 -14
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +302 -51
- package/src/notifications/telegram-reference.ts +0 -16
- 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 +29 -17
- 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/computer.md +2 -2
- 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/runtime-api-key.ts +16 -0
- package/src/sdk.ts +52 -28
- package/src/session/agent-session.ts +1011 -141
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/skc-runtime/deep-interview-recorder.ts +62 -8
- package/src/skc-runtime/deep-interview-state.ts +44 -2
- package/src/skc-runtime/session-state-sidecar.ts +263 -20
- package/src/skc-runtime/state-runtime.ts +12 -2
- package/src/skc-runtime/state-writer.ts +15 -0
- package/src/skc-runtime/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1663 -159
- package/src/skc-runtime/workflow-manifest.generated.json +113 -2
- package/src/skc-runtime/workflow-manifest.ts +74 -1
- 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 +218 -71
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- 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/computer.ts +246 -43
- 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/tools/telegram-send.ts +6 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- 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/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
package/src/task/executor.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { AuthStorage } from "../session/auth-storage";
|
|
|
34
34
|
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
35
35
|
import { SessionManager } from "../session/session-manager";
|
|
36
36
|
import { truncateTail } from "../session/streaming-output";
|
|
37
|
+
import { sessionRoot } from "../skc-runtime/session-layout";
|
|
37
38
|
import type { ContextFileEntry } from "../tools";
|
|
38
39
|
import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
39
40
|
import { type ReportFindingDetails, toReviewFinding } from "../tools/review";
|
|
@@ -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,
|
|
@@ -139,6 +141,8 @@ export interface ExecutorOptions {
|
|
|
139
141
|
artifactsDir?: string;
|
|
140
142
|
/** Path to parent conversation context file */
|
|
141
143
|
contextFile?: string;
|
|
144
|
+
/** Whether the parent runtime actually exposes IRC coordination. */
|
|
145
|
+
ircAvailable?: boolean;
|
|
142
146
|
eventBus?: EventBus;
|
|
143
147
|
contextFiles?: ContextFileEntry[];
|
|
144
148
|
skills?: Skill[];
|
|
@@ -243,6 +247,51 @@ function previewOffendingData(value: unknown, maxLength = 500): string {
|
|
|
243
247
|
}
|
|
244
248
|
return serialized.length > maxLength ? `${serialized.slice(0, maxLength)}…` : serialized;
|
|
245
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
|
+
}
|
|
246
295
|
|
|
247
296
|
function tryParseJsonOutput(text: string): unknown | undefined {
|
|
248
297
|
const trimmed = text.trim();
|
|
@@ -319,6 +368,8 @@ interface FinalizeSubprocessOutputResult {
|
|
|
319
368
|
export const SUBAGENT_WARNING_NULL_YIELD = "SYSTEM WARNING: Subagent called yield with null data.";
|
|
320
369
|
export const SUBAGENT_WARNING_MISSING_YIELD =
|
|
321
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.";
|
|
322
373
|
|
|
323
374
|
/** Build a schema_violation outcome — surfaced as a non-zero exit so callers treat it as a failure. */
|
|
324
375
|
function buildSchemaViolationOutcome(
|
|
@@ -345,6 +396,21 @@ function buildSchemaViolationOutcome(
|
|
|
345
396
|
return { rawOutput, stderr: headline, exitCode: 1 };
|
|
346
397
|
}
|
|
347
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
|
+
|
|
348
414
|
export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): FinalizeSubprocessOutputResult {
|
|
349
415
|
let { rawOutput, exitCode, stderr } = args;
|
|
350
416
|
const { yieldItems, reportFindings, doneAborted, signalAborted, outputSchema } = args;
|
|
@@ -374,21 +440,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
374
440
|
stderr = `schema_violation: invalid output schema: ${schemaError}`;
|
|
375
441
|
exitCode = 1;
|
|
376
442
|
} else {
|
|
377
|
-
const
|
|
378
|
-
if (
|
|
379
|
-
const outcome =
|
|
443
|
+
const placeholderPath = findPlaceholderYieldPath(completeData);
|
|
444
|
+
if (placeholderPath) {
|
|
445
|
+
const outcome = buildPlaceholderYieldOutcome(placeholderPath, completeData);
|
|
380
446
|
rawOutput = outcome.rawOutput;
|
|
381
447
|
stderr = outcome.stderr;
|
|
382
448
|
exitCode = outcome.exitCode;
|
|
383
449
|
} else {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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 = "";
|
|
389
465
|
}
|
|
390
|
-
exitCode = 0;
|
|
391
|
-
stderr = "";
|
|
392
466
|
}
|
|
393
467
|
}
|
|
394
468
|
}
|
|
@@ -401,21 +475,29 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
401
475
|
if (fallback) {
|
|
402
476
|
const completeData = normalizeCompleteData(fallback.data, reportFindings);
|
|
403
477
|
const { validator } = buildOutputValidator(outputSchema);
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
406
|
-
const outcome =
|
|
478
|
+
const placeholderPath = findPlaceholderYieldPath(completeData);
|
|
479
|
+
if (placeholderPath) {
|
|
480
|
+
const outcome = buildPlaceholderYieldOutcome(placeholderPath, completeData);
|
|
407
481
|
rawOutput = outcome.rawOutput;
|
|
408
482
|
stderr = outcome.stderr;
|
|
409
483
|
exitCode = outcome.exitCode;
|
|
410
484
|
} else {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 = "";
|
|
416
500
|
}
|
|
417
|
-
exitCode = 0;
|
|
418
|
-
stderr = "";
|
|
419
501
|
}
|
|
420
502
|
} else if (!hasOutputSchema && allowFallback && rawOutput.trim().length > 0) {
|
|
421
503
|
exitCode = 0;
|
|
@@ -623,7 +705,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
623
705
|
: agent.spawns.join(",");
|
|
624
706
|
|
|
625
707
|
const lspEnabled = enableLsp ?? true;
|
|
626
|
-
const ircEnabled =
|
|
708
|
+
const ircEnabled = options.ircAvailable === true;
|
|
627
709
|
const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
|
|
628
710
|
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
|
|
629
711
|
|
|
@@ -1207,6 +1289,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1207
1289
|
const subagentAgentIdentity: AgentIdentity | undefined = options.parentTelemetry
|
|
1208
1290
|
? { id, name: agent.name, description: agent.description }
|
|
1209
1291
|
: undefined;
|
|
1292
|
+
let subagentTokenTurn = 0;
|
|
1293
|
+
const tokenLogDir = options.parentSessionId
|
|
1294
|
+
? path.join(sessionRoot(cwd, options.parentSessionId), "token-logs")
|
|
1295
|
+
: undefined;
|
|
1210
1296
|
const subagentTelemetry: AgentTelemetryConfig | undefined =
|
|
1211
1297
|
options.parentTelemetry && subagentAgentIdentity
|
|
1212
1298
|
? {
|
|
@@ -1215,6 +1301,27 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1215
1301
|
// Clear parent's conversationId; the child loop falls back to
|
|
1216
1302
|
// its own AgentLoopConfig.sessionId.
|
|
1217
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
|
+
},
|
|
1218
1325
|
}
|
|
1219
1326
|
: undefined;
|
|
1220
1327
|
|
|
@@ -1231,9 +1338,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1231
1338
|
|
|
1232
1339
|
const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
|
|
1233
1340
|
|
|
1234
|
-
const forkContextNotice =
|
|
1235
|
-
|
|
1236
|
-
|
|
1341
|
+
const forkContextNotice =
|
|
1342
|
+
options.forkContextSeed && options.forkContextSeed.metadata.includedMessages > 0
|
|
1343
|
+
? `This subagent was started with a forked snapshot of the parent conversation. Included ${options.forkContextSeed.metadata.includedMessages} message(s), skipped ${options.forkContextSeed.metadata.skippedMessages}, approximately ${options.forkContextSeed.metadata.approximateTokens} tokens. The snapshot is not live.${ircEnabled ? " Use IRC for live coordination." : " Rely on the explicit assignment and supplied context for coordination."}`
|
|
1344
|
+
: "";
|
|
1237
1345
|
|
|
1238
1346
|
const agentSubskillBlock = await buildAgentSubskillInjection({
|
|
1239
1347
|
cwd,
|
|
@@ -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";
|
|
@@ -178,6 +179,10 @@ export {
|
|
|
178
179
|
/**
|
|
179
180
|
* Render the tool description from a cached agent list and current settings.
|
|
180
181
|
*/
|
|
182
|
+
function hasAvailableIrcTool(session: ToolSession): boolean {
|
|
183
|
+
return session.settings.get("irc.enabled") === true && session.getToolByName?.("irc") !== undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
181
186
|
function renderDescription(
|
|
182
187
|
agents: AgentDefinition[],
|
|
183
188
|
maxConcurrency: number,
|
|
@@ -299,11 +304,11 @@ function resolveForkSeedParamsForMode(
|
|
|
299
304
|
case "none":
|
|
300
305
|
return undefined;
|
|
301
306
|
case "receipt":
|
|
302
|
-
return { maxMessages: 1, maxTokens:
|
|
307
|
+
return { maxMessages: 1, maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE.receipt };
|
|
303
308
|
case "last-turn":
|
|
304
|
-
return { maxMessages: 2, maxTokens:
|
|
309
|
+
return { maxMessages: 2, maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE["last-turn"] };
|
|
305
310
|
case "bounded":
|
|
306
|
-
return { maxMessages: capMessages(50), maxTokens:
|
|
311
|
+
return { maxMessages: capMessages(50), maxTokens: FORK_CONTEXT_TOKEN_BUDGET_BY_MODE.bounded };
|
|
307
312
|
case "full":
|
|
308
313
|
return { maxMessages: capMessages(500), maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, model) };
|
|
309
314
|
default:
|
|
@@ -381,7 +386,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
381
386
|
true,
|
|
382
387
|
disabledAgents,
|
|
383
388
|
this.#getTaskSimpleMode(),
|
|
384
|
-
this.session
|
|
389
|
+
hasAvailableIrcTool(this.session),
|
|
385
390
|
this.session.getSessionSpawns() ?? "*",
|
|
386
391
|
);
|
|
387
392
|
}
|
|
@@ -814,7 +819,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
814
819
|
? ` Failed to schedule ${failedSchedules.length} task${failedSchedules.length === 1 ? "" : "s"}.`
|
|
815
820
|
: "";
|
|
816
821
|
|
|
817
|
-
const ircEnabled = this.session
|
|
822
|
+
const ircEnabled = hasAvailableIrcTool(this.session);
|
|
818
823
|
const taskIdByItemId = new Map<string, string>();
|
|
819
824
|
for (let i = 0; i < taskItems.length; i++) {
|
|
820
825
|
taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
|
|
@@ -1171,7 +1176,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1171
1176
|
// Write parent conversation context for subagents. When IRC is available,
|
|
1172
1177
|
// subagents should ask live peers instead of reading a stale markdown dump.
|
|
1173
1178
|
await fs.mkdir(effectiveArtifactsDir, { recursive: true });
|
|
1174
|
-
const shouldWriteConversationContext = this.session
|
|
1179
|
+
const shouldWriteConversationContext = !hasAvailableIrcTool(this.session);
|
|
1175
1180
|
const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
|
|
1176
1181
|
let contextFilePath: string | undefined;
|
|
1177
1182
|
if (compactContext) {
|
|
@@ -1298,6 +1303,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1298
1303
|
persistArtifacts: !!artifactsDir,
|
|
1299
1304
|
artifactsDir: effectiveArtifactsDir,
|
|
1300
1305
|
contextFile: contextFilePath,
|
|
1306
|
+
ircAvailable: hasAvailableIrcTool(this.session),
|
|
1301
1307
|
enableLsp: subagentLspEnabled,
|
|
1302
1308
|
signal,
|
|
1303
1309
|
eventBus: this.session.eventBus,
|
|
@@ -1360,6 +1366,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1360
1366
|
persistArtifacts: !!artifactsDir,
|
|
1361
1367
|
artifactsDir: effectiveArtifactsDir,
|
|
1362
1368
|
contextFile: contextFilePath,
|
|
1369
|
+
ircAvailable: hasAvailableIrcTool(this.session),
|
|
1363
1370
|
enableLsp: subagentLspEnabled,
|
|
1364
1371
|
signal,
|
|
1365
1372
|
eventBus: this.session.eventBus,
|
|
@@ -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
|
*
|
|
@@ -24,7 +26,7 @@ function escapeRegExp(value: string): string {
|
|
|
24
26
|
*/
|
|
25
27
|
export class AgentOutputManager {
|
|
26
28
|
#nextId = 0;
|
|
27
|
-
#
|
|
29
|
+
#initPromise: Promise<void> | undefined;
|
|
28
30
|
readonly #getArtifactsDir: () => string | null;
|
|
29
31
|
readonly #parentPrefix: string | undefined;
|
|
30
32
|
|
|
@@ -33,14 +35,24 @@ export class AgentOutputManager {
|
|
|
33
35
|
this.#parentPrefix = options?.parentPrefix;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Memoize the in-flight scan so concurrent `allocate*`/`peekNextIndex`
|
|
40
|
+
* calls await the SAME `readdir` before `#nextId` is derived. Assigning the
|
|
41
|
+
* promise synchronously (before any `await`) closes the TOCTOU window where a
|
|
42
|
+
* boolean "initialized" flag, flipped ahead of the awaited scan, let a second
|
|
43
|
+
* caller allocate at `#nextId === 0` while the first was still scanning —
|
|
44
|
+
* producing duplicate indices that overwrite prior outputs on resume.
|
|
45
|
+
*/
|
|
46
|
+
#ensureInitialized(): Promise<void> {
|
|
47
|
+
this.#initPromise ??= this.#scanExistingOutputs();
|
|
48
|
+
return this.#initPromise;
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
/**
|
|
37
52
|
* Scan existing agent output files to find the next available ID.
|
|
38
53
|
* This ensures we don't overwrite outputs when resuming a session.
|
|
39
54
|
*/
|
|
40
|
-
async #
|
|
41
|
-
if (this.#initialized) return;
|
|
42
|
-
this.#initialized = true;
|
|
43
|
-
|
|
55
|
+
async #scanExistingOutputs(): Promise<void> {
|
|
44
56
|
const dir = this.#getArtifactsDir();
|
|
45
57
|
if (!dir) return;
|
|
46
58
|
|
|
@@ -52,8 +64,8 @@ export class AgentOutputManager {
|
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
const pattern = this.#parentPrefix
|
|
55
|
-
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)
|
|
56
|
-
:
|
|
67
|
+
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`)
|
|
68
|
+
: new RegExp(`^(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`);
|
|
57
69
|
|
|
58
70
|
let maxId = -1;
|
|
59
71
|
for (const file of files) {
|
|
@@ -103,6 +115,6 @@ export class AgentOutputManager {
|
|
|
103
115
|
*/
|
|
104
116
|
reset(): void {
|
|
105
117
|
this.#nextId = 0;
|
|
106
|
-
this.#
|
|
118
|
+
this.#initPromise = undefined;
|
|
107
119
|
}
|
|
108
120
|
}
|
package/src/task/receipt.ts
CHANGED
|
@@ -77,6 +77,28 @@ function truncateText(value: string | undefined, maxChars: number): string | und
|
|
|
77
77
|
return value.length > maxChars ? value.slice(0, maxChars) : value;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
const SAFE_REVIEW_SEVERITIES = new Set(["blocker", "critical", "error", "high", "medium", "warning", "low", "info"]);
|
|
81
|
+
const SAFE_REVIEW_PRIORITIES = new Set(["P0", "P1", "P2", "P3"]);
|
|
82
|
+
|
|
83
|
+
function normalizeReviewFindingSeverity(severity: unknown, priority: unknown): string | undefined {
|
|
84
|
+
if (typeof severity === "string") {
|
|
85
|
+
const normalizedPriority = severity.toUpperCase();
|
|
86
|
+
if (SAFE_REVIEW_PRIORITIES.has(normalizedPriority)) return normalizedPriority;
|
|
87
|
+
const normalizedSeverity = severity.toLowerCase();
|
|
88
|
+
if (SAFE_REVIEW_SEVERITIES.has(normalizedSeverity)) return normalizedSeverity;
|
|
89
|
+
}
|
|
90
|
+
if (typeof priority === "string") {
|
|
91
|
+
const normalizedPriority = priority.toUpperCase();
|
|
92
|
+
if (SAFE_REVIEW_PRIORITIES.has(normalizedPriority)) return normalizedPriority;
|
|
93
|
+
const normalizedSeverity = priority.toLowerCase();
|
|
94
|
+
if (SAFE_REVIEW_SEVERITIES.has(normalizedSeverity)) return normalizedSeverity;
|
|
95
|
+
}
|
|
96
|
+
if (typeof priority === "number" && Number.isInteger(priority) && priority >= 0 && priority <= 3) {
|
|
97
|
+
return `P${priority}`;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outputRef"]): string {
|
|
81
103
|
const status = getStatus(raw);
|
|
82
104
|
if (raw.modelSubstitutionWarning) {
|
|
@@ -117,19 +139,16 @@ function buildReview(raw: SingleResult): TaskResultReceipt["review"] | undefined
|
|
|
117
139
|
const rawFindings = Array.isArray(data.report_finding) ? data.report_finding : [];
|
|
118
140
|
const findings = rawFindings.slice(0, 20).map(item => {
|
|
119
141
|
const value = item && typeof item === "object" ? (item as Record<string, unknown>) : {};
|
|
120
|
-
const severity =
|
|
121
|
-
typeof value.severity === "string"
|
|
122
|
-
? value.severity
|
|
123
|
-
: typeof value.priority === "string"
|
|
124
|
-
? value.priority
|
|
125
|
-
: undefined;
|
|
142
|
+
const severity = normalizeReviewFindingSeverity(value.severity, value.priority);
|
|
126
143
|
const summaryValue = value.summary ?? value.title ?? value.message ?? value.body ?? "finding";
|
|
127
144
|
return { severity, summary: truncateText(String(summaryValue), 200) ?? "finding" };
|
|
128
145
|
});
|
|
129
146
|
if (!reviewYield && findings.length === 0) return undefined;
|
|
130
147
|
return {
|
|
131
|
-
overallCorrectness:
|
|
148
|
+
overallCorrectness: truncateText(
|
|
132
149
|
typeof reviewYield?.overall_correctness === "string" ? reviewYield.overall_correctness : undefined,
|
|
150
|
+
200,
|
|
151
|
+
),
|
|
133
152
|
findingCount: rawFindings.length,
|
|
134
153
|
findings: findings.length > 0 ? findings : undefined,
|
|
135
154
|
};
|
|
@@ -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 { SKC_SESSION_ACTIVITY_FILE, sessionRoot } from "../skc-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(), "skc-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), SKC_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
|
+
});
|