@caupulican/pi-adaptative 0.81.6 → 0.81.7

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/core/background-lane-controller.d.ts +1 -0
  3. package/dist/core/background-lane-controller.d.ts.map +1 -1
  4. package/dist/core/background-lane-controller.js +7 -3
  5. package/dist/core/background-lane-controller.js.map +1 -1
  6. package/dist/core/bash-execution-controller.d.ts +1 -1
  7. package/dist/core/bash-execution-controller.d.ts.map +1 -1
  8. package/dist/core/bash-execution-controller.js +9 -6
  9. package/dist/core/bash-execution-controller.js.map +1 -1
  10. package/dist/core/exec.d.ts.map +1 -1
  11. package/dist/core/exec.js +7 -2
  12. package/dist/core/exec.js.map +1 -1
  13. package/dist/core/extensions/runner.d.ts.map +1 -1
  14. package/dist/core/extensions/runner.js +19 -5
  15. package/dist/core/extensions/runner.js.map +1 -1
  16. package/dist/core/failure-corpus.d.ts.map +1 -1
  17. package/dist/core/failure-corpus.js +1 -1
  18. package/dist/core/failure-corpus.js.map +1 -1
  19. package/dist/core/learning/reflection-engine.d.ts.map +1 -1
  20. package/dist/core/learning/reflection-engine.js +3 -1
  21. package/dist/core/learning/reflection-engine.js.map +1 -1
  22. package/dist/core/scout-controller.d.ts +5 -0
  23. package/dist/core/scout-controller.d.ts.map +1 -1
  24. package/dist/core/scout-controller.js +4 -1
  25. package/dist/core/scout-controller.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts.map +1 -1
  27. package/dist/core/settings-manager.js +1 -1
  28. package/dist/core/settings-manager.js.map +1 -1
  29. package/dist/migrations.d.ts +1 -0
  30. package/dist/migrations.d.ts.map +1 -1
  31. package/dist/migrations.js +7 -1
  32. package/dist/migrations.js.map +1 -1
  33. package/dist/modes/interactive/auto-learn-controller.d.ts.map +1 -1
  34. package/dist/modes/interactive/auto-learn-controller.js +14 -6
  35. package/dist/modes/interactive/auto-learn-controller.js.map +1 -1
  36. package/dist/modes/interactive/components/session-selector.d.ts +1 -0
  37. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  38. package/dist/modes/interactive/components/session-selector.js +11 -4
  39. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  40. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  41. package/dist/modes/interactive/interactive-mode.js +5 -0
  42. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  43. package/dist/utils/tools-manager.d.ts +1 -0
  44. package/dist/utils/tools-manager.d.ts.map +1 -1
  45. package/dist/utils/tools-manager.js +14 -1
  46. package/dist/utils/tools-manager.js.map +1 -1
  47. package/docs/bug-ledger.md +22 -22
  48. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  49. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  50. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  51. package/examples/extensions/sandbox/package-lock.json +2 -2
  52. package/examples/extensions/sandbox/package.json +1 -1
  53. package/examples/extensions/with-deps/package-lock.json +2 -2
  54. package/examples/extensions/with-deps/package.json +1 -1
  55. package/npm-shrinkwrap.json +12 -12
  56. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"scout-controller.d.ts","sourceRoot":"","sources":["../../src/core/scout-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,eAAO,MAAM,mBAAmB,65BAewC,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,iBAAiB,IAAI,OAAO,CAC3B;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAC9F,CAAC;IACF,MAAM,IAAI,MAAM,CAAC;IACjB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IAClD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7C,OAAO,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,YAAY,GAAG,WAAW,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAOD,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAE3C,YAAY,IAAI,EAAE,mBAAmB,EAEpC;IAEK,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAiF9E;CACD;AAED,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EACrC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC9C,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC,CAuB7D","sourcesContent":["import { Agent, type AgentTool, type StreamFn } from \"@caupulican/pi-agent-core\";\nimport type { Model } from \"@caupulican/pi-ai\";\n\nexport const SCOUT_SYSTEM_PROMPT = `You are a repository scout. You explore a codebase with read-only tools and return compact evidence. You do NOT solve tasks, write code, or modify anything.\n\nTOOLS: read (file contents), grep (search file contents), find (locate files by glob/name).\n- Issue INDEPENDENT tool calls in parallel in the same turn whenever possible.\n- Prefer grep/find to narrow, then read only the decisive regions.\n\nOUTPUT CONTRACT: when you have enough evidence (or hit your turn budget), reply with:\n<final_answer>\n<one-to-three-sentence summary of what you found>\npath/to/file.ts:START-END\npath/to/other.ts:START-END\n</final_answer>\n- Every path MUST be one you actually observed via a tool result this run.\n- Line ranges point at the decisive code, not whole files.\n- If you found nothing relevant, say so inside <final_answer> with zero citations.\nTurn budget: {MAX_TURNS} turns. Be fast; be precise; cite, don't paste.`;\n\nexport interface ScoutCitation {\n\tpath: string;\n\tstart: number;\n\tend: number;\n\tvalid: boolean;\n}\n\nexport interface ScoutRunResult {\n\tsummary: string;\n\tcitations: ScoutCitation[];\n\tdroppedCitations: number;\n\tunreliable: boolean;\n\ttruncated: boolean;\n\tturnsUsed: number;\n\tfailure?: string;\n}\n\nexport interface ScoutControllerDeps {\n\tresolveScoutModel(): Promise<\n\t\t{ model: Model<any>; apiKey?: string; headers?: Record<string, string> } | { failure: string }\n\t>;\n\tgetCwd(): string;\n\tbuildReadOnlyTools(cwd: string): AgentTool<any>[];\n\tstreamFn?: StreamFn;\n\tfileExists(path: string): boolean;\n\tcountLines(path: string): number | undefined;\n\tonEvent?(event: { type: \"scout_turn\" | \"scout_end\"; detail: string }): void;\n\tsignal?: AbortSignal;\n}\n\nconst MAX_TURNS_DEFAULT = 8;\nconst MAX_TURNS_HARD_CAP = 12;\nconst MAX_OUTPUT_TOKENS = 30_000;\nconst READ_ONLY_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\"]);\n\nexport class ScoutController {\n\tprivate readonly deps: ScoutControllerDeps;\n\n\tconstructor(deps: ScoutControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync run(query: string, maxTurns = MAX_TURNS_DEFAULT): Promise<ScoutRunResult> {\n\t\tconst turnLimit = clampTurns(maxTurns);\n\t\tconst unavailable = emptyResult();\n\t\tif (this.deps.signal?.aborted) {\n\t\t\treturn { ...unavailable, failure: \"aborted\" };\n\t\t}\n\n\t\tlet modelResolution: Awaited<ReturnType<ScoutControllerDeps[\"resolveScoutModel\"]>>;\n\t\ttry {\n\t\t\tmodelResolution = await this.deps.resolveScoutModel();\n\t\t} catch (error) {\n\t\t\treturn { ...unavailable, failure: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t\tif (\"failure\" in modelResolution) {\n\t\t\treturn { ...unavailable, failure: modelResolution.failure };\n\t\t}\n\n\t\tlet turnsUsed = 0;\n\t\tlet truncated = false;\n\t\tlet lastAssistantText = \"\";\n\t\tlet outputTokens = 0;\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tmodel: modelResolution.model,\n\t\t\t\tsystemPrompt: SCOUT_SYSTEM_PROMPT.replace(\"{MAX_TURNS}\", String(turnLimit)),\n\t\t\t\ttools: this.deps\n\t\t\t\t\t.buildReadOnlyTools(this.deps.getCwd())\n\t\t\t\t\t.filter((tool) => READ_ONLY_TOOL_NAMES.has(tool.name)),\n\t\t\t},\n\t\t\tstreamFn: this.deps.streamFn,\n\t\t\tgetApiKey: () => modelResolution.apiKey,\n\t\t\tmaxStallTurns: turnLimit,\n\t\t});\n\n\t\tconst unsubscribe = agent.subscribe((event) => {\n\t\t\tif (event.type !== \"message_end\" || event.message.role !== \"assistant\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tturnsUsed += 1;\n\t\t\tlastAssistantText = assistantText(event.message);\n\t\t\toutputTokens += event.message.usage?.totalTokens ?? 0;\n\t\t\tthis.deps.onEvent?.({ type: \"scout_turn\", detail: `turn ${turnsUsed}` });\n\t\t\tif (hasFinalAnswer(lastAssistantText)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (turnsUsed >= turnLimit || outputTokens >= MAX_OUTPUT_TOKENS) {\n\t\t\t\ttruncated = true;\n\t\t\t\tagent.abort();\n\t\t\t}\n\t\t});\n\n\t\tconst abortScout = (): void => {\n\t\t\ttruncated = true;\n\t\t\tagent.abort();\n\t\t};\n\t\tthis.deps.signal?.addEventListener(\"abort\", abortScout, { once: true });\n\t\tlet runFailure: string | undefined;\n\t\ttry {\n\t\t\tawait agent.prompt(query);\n\t\t} catch (error) {\n\t\t\tif (!truncated && !this.deps.signal?.aborted) {\n\t\t\t\trunFailure = error instanceof Error ? error.message : String(error);\n\t\t\t}\n\t\t} finally {\n\t\t\tunsubscribe();\n\t\t\tthis.deps.signal?.removeEventListener(\"abort\", abortScout);\n\t\t}\n\n\t\tconst result = parseScoutAnswer(\n\t\t\tlastAssistantText,\n\t\t\t(path) => this.deps.fileExists(path),\n\t\t\t(path) => this.deps.countLines(path),\n\t\t);\n\t\tconst finalResult = {\n\t\t\t...result,\n\t\t\ttruncated: truncated || !hasFinalAnswer(lastAssistantText),\n\t\t\tturnsUsed,\n\t\t\tfailure: this.deps.signal?.aborted ? \"aborted\" : runFailure,\n\t\t};\n\t\tthis.deps.onEvent?.({ type: \"scout_end\", detail: finalResult.failure ?? \"ok\" });\n\t\treturn finalResult;\n\t}\n}\n\nexport function parseScoutAnswer(\n\ttext: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): Omit<ScoutRunResult, \"truncated\" | \"turnsUsed\" | \"failure\"> {\n\tconst finalAnswer = /<final_answer>([\\s\\S]*?)<\\/final_answer>/i.exec(text)?.[1] ?? text;\n\tconst summaryLines: string[] = [];\n\tconst citations: ScoutCitation[] = [];\n\n\tfor (const rawLine of finalAnswer.split(/\\r?\\n/)) {\n\t\tconst line = rawLine.trim();\n\t\tif (!line) continue;\n\t\tconst citation = parseCitationLine(line, fileExists, countLines);\n\t\tif (citation) {\n\t\t\tcitations.push(citation);\n\t\t\tcontinue;\n\t\t}\n\t\tsummaryLines.push(line);\n\t}\n\n\tconst droppedCitations = citations.filter((citation) => !citation.valid).length;\n\treturn {\n\t\tsummary: summaryLines.join(\"\\n\"),\n\t\tcitations,\n\t\tdroppedCitations,\n\t\tunreliable: citations.length > 0 && droppedCitations / citations.length > 0.5,\n\t};\n}\n\nfunction parseCitationLine(\n\tline: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): ScoutCitation | undefined {\n\tconst match = /^(.+?):(\\d+)(?:-(\\d+))?$/.exec(line);\n\tif (!match) return undefined;\n\tconst path = match[1].trim();\n\tconst start = Number.parseInt(match[2], 10);\n\tconst end = Number.parseInt(match[3] ?? match[2], 10);\n\tconst lineCount = countLines(path);\n\tconst valid =\n\t\tfileExists(path) &&\n\t\tNumber.isFinite(start) &&\n\t\tNumber.isFinite(end) &&\n\t\tstart >= 1 &&\n\t\tend >= start &&\n\t\t(lineCount === undefined || end <= lineCount);\n\treturn { path, start, end, valid };\n}\n\nfunction clampTurns(maxTurns: number): number {\n\tif (!Number.isFinite(maxTurns)) return MAX_TURNS_DEFAULT;\n\treturn Math.min(MAX_TURNS_HARD_CAP, Math.max(1, Math.trunc(maxTurns)));\n}\n\nfunction emptyResult(): ScoutRunResult {\n\treturn { summary: \"\", citations: [], droppedCitations: 0, unreliable: false, truncated: false, turnsUsed: 0 };\n}\n\nfunction hasFinalAnswer(text: string): boolean {\n\treturn /<final_answer>[\\s\\S]*?<\\/final_answer>/i.test(text);\n}\n\nfunction assistantText(message: { content: unknown }): string {\n\tif (!Array.isArray(message.content)) return \"\";\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } =>\n\t\t\tBoolean(\n\t\t\t\tcontent &&\n\t\t\t\t\ttypeof content === \"object\" &&\n\t\t\t\t\t(content as { type?: unknown }).type === \"text\" &&\n\t\t\t\t\ttypeof (content as { text?: unknown }).text === \"string\",\n\t\t\t),\n\t\t)\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"scout-controller.d.ts","sourceRoot":"","sources":["../../src/core/scout-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,eAAO,MAAM,mBAAmB,65BAewC,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IACnC,iBAAiB,IAAI,OAAO,CAC3B;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAC9F,CAAC;IACF,MAAM,IAAI,MAAM,CAAC;IACjB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IAClD,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7C,OAAO,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,YAAY,GAAG,WAAW,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAOD,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAsB;IAE3C,YAAY,IAAI,EAAE,mBAAmB,EAEpC;IAEK,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAiF9E;CACD;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAAG,MAAM,CAEzF;AAED,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EACrC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC9C,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC,CAuB7D","sourcesContent":["import { Agent, type AgentTool, type StreamFn } from \"@caupulican/pi-agent-core\";\nimport type { Model } from \"@caupulican/pi-ai\";\n\nexport const SCOUT_SYSTEM_PROMPT = `You are a repository scout. You explore a codebase with read-only tools and return compact evidence. You do NOT solve tasks, write code, or modify anything.\n\nTOOLS: read (file contents), grep (search file contents), find (locate files by glob/name).\n- Issue INDEPENDENT tool calls in parallel in the same turn whenever possible.\n- Prefer grep/find to narrow, then read only the decisive regions.\n\nOUTPUT CONTRACT: when you have enough evidence (or hit your turn budget), reply with:\n<final_answer>\n<one-to-three-sentence summary of what you found>\npath/to/file.ts:START-END\npath/to/other.ts:START-END\n</final_answer>\n- Every path MUST be one you actually observed via a tool result this run.\n- Line ranges point at the decisive code, not whole files.\n- If you found nothing relevant, say so inside <final_answer> with zero citations.\nTurn budget: {MAX_TURNS} turns. Be fast; be precise; cite, don't paste.`;\n\nexport interface ScoutCitation {\n\tpath: string;\n\tstart: number;\n\tend: number;\n\tvalid: boolean;\n}\n\nexport interface ScoutRunResult {\n\tsummary: string;\n\tcitations: ScoutCitation[];\n\tdroppedCitations: number;\n\tunreliable: boolean;\n\ttruncated: boolean;\n\tturnsUsed: number;\n\tfailure?: string;\n}\n\nexport interface ScoutControllerDeps {\n\tresolveScoutModel(): Promise<\n\t\t{ model: Model<any>; apiKey?: string; headers?: Record<string, string> } | { failure: string }\n\t>;\n\tgetCwd(): string;\n\tbuildReadOnlyTools(cwd: string): AgentTool<any>[];\n\tstreamFn?: StreamFn;\n\tfileExists(path: string): boolean;\n\tcountLines(path: string): number | undefined;\n\tonEvent?(event: { type: \"scout_turn\" | \"scout_end\"; detail: string }): void;\n\tsignal?: AbortSignal;\n}\n\nconst MAX_TURNS_DEFAULT = 8;\nconst MAX_TURNS_HARD_CAP = 12;\nconst MAX_OUTPUT_TOKENS = 30_000;\nconst READ_ONLY_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\"]);\n\nexport class ScoutController {\n\tprivate readonly deps: ScoutControllerDeps;\n\n\tconstructor(deps: ScoutControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync run(query: string, maxTurns = MAX_TURNS_DEFAULT): Promise<ScoutRunResult> {\n\t\tconst turnLimit = clampTurns(maxTurns);\n\t\tconst unavailable = emptyResult();\n\t\tif (this.deps.signal?.aborted) {\n\t\t\treturn { ...unavailable, failure: \"aborted\" };\n\t\t}\n\n\t\tlet modelResolution: Awaited<ReturnType<ScoutControllerDeps[\"resolveScoutModel\"]>>;\n\t\ttry {\n\t\t\tmodelResolution = await this.deps.resolveScoutModel();\n\t\t} catch (error) {\n\t\t\treturn { ...unavailable, failure: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t\tif (\"failure\" in modelResolution) {\n\t\t\treturn { ...unavailable, failure: modelResolution.failure };\n\t\t}\n\n\t\tlet turnsUsed = 0;\n\t\tlet truncated = false;\n\t\tlet lastAssistantText = \"\";\n\t\tlet outputTokens = 0;\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tmodel: modelResolution.model,\n\t\t\t\tsystemPrompt: SCOUT_SYSTEM_PROMPT.replace(\"{MAX_TURNS}\", String(turnLimit)),\n\t\t\t\ttools: this.deps\n\t\t\t\t\t.buildReadOnlyTools(this.deps.getCwd())\n\t\t\t\t\t.filter((tool) => READ_ONLY_TOOL_NAMES.has(tool.name)),\n\t\t\t},\n\t\t\tstreamFn: this.deps.streamFn,\n\t\t\tgetApiKey: () => modelResolution.apiKey,\n\t\t\tmaxStallTurns: turnLimit,\n\t\t});\n\n\t\tconst unsubscribe = agent.subscribe((event) => {\n\t\t\tif (event.type !== \"message_end\" || event.message.role !== \"assistant\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tturnsUsed += 1;\n\t\t\tlastAssistantText = assistantText(event.message);\n\t\t\toutputTokens += getScoutOutputTokenCount(event.message);\n\t\t\tthis.deps.onEvent?.({ type: \"scout_turn\", detail: `turn ${turnsUsed}` });\n\t\t\tif (hasFinalAnswer(lastAssistantText)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (turnsUsed >= turnLimit || outputTokens >= MAX_OUTPUT_TOKENS) {\n\t\t\t\ttruncated = true;\n\t\t\t\tagent.abort();\n\t\t\t}\n\t\t});\n\n\t\tconst abortScout = (): void => {\n\t\t\ttruncated = true;\n\t\t\tagent.abort();\n\t\t};\n\t\tthis.deps.signal?.addEventListener(\"abort\", abortScout, { once: true });\n\t\tlet runFailure: string | undefined;\n\t\ttry {\n\t\t\tawait agent.prompt(query);\n\t\t} catch (error) {\n\t\t\tif (!truncated && !this.deps.signal?.aborted) {\n\t\t\t\trunFailure = error instanceof Error ? error.message : String(error);\n\t\t\t}\n\t\t} finally {\n\t\t\tunsubscribe();\n\t\t\tthis.deps.signal?.removeEventListener(\"abort\", abortScout);\n\t\t}\n\n\t\tconst result = parseScoutAnswer(\n\t\t\tlastAssistantText,\n\t\t\t(path) => this.deps.fileExists(path),\n\t\t\t(path) => this.deps.countLines(path),\n\t\t);\n\t\tconst finalResult = {\n\t\t\t...result,\n\t\t\ttruncated: truncated || !hasFinalAnswer(lastAssistantText),\n\t\t\tturnsUsed,\n\t\t\tfailure: this.deps.signal?.aborted ? \"aborted\" : runFailure,\n\t\t};\n\t\tthis.deps.onEvent?.({ type: \"scout_end\", detail: finalResult.failure ?? \"ok\" });\n\t\treturn finalResult;\n\t}\n}\n\nexport function getScoutOutputTokenCount(message: { usage?: { output?: number } }): number {\n\treturn message.usage?.output ?? 0;\n}\n\nexport function parseScoutAnswer(\n\ttext: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): Omit<ScoutRunResult, \"truncated\" | \"turnsUsed\" | \"failure\"> {\n\tconst finalAnswer = /<final_answer>([\\s\\S]*?)<\\/final_answer>/i.exec(text)?.[1] ?? text;\n\tconst summaryLines: string[] = [];\n\tconst citations: ScoutCitation[] = [];\n\n\tfor (const rawLine of finalAnswer.split(/\\r?\\n/)) {\n\t\tconst line = rawLine.trim();\n\t\tif (!line) continue;\n\t\tconst citation = parseCitationLine(line, fileExists, countLines);\n\t\tif (citation) {\n\t\t\tcitations.push(citation);\n\t\t\tcontinue;\n\t\t}\n\t\tsummaryLines.push(line);\n\t}\n\n\tconst droppedCitations = citations.filter((citation) => !citation.valid).length;\n\treturn {\n\t\tsummary: summaryLines.join(\"\\n\"),\n\t\tcitations,\n\t\tdroppedCitations,\n\t\tunreliable: citations.length > 0 && droppedCitations / citations.length > 0.5,\n\t};\n}\n\nfunction parseCitationLine(\n\tline: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): ScoutCitation | undefined {\n\tconst match = /^(.+?):(\\d+)(?:-(\\d+))?$/.exec(line);\n\tif (!match) return undefined;\n\tconst path = match[1].trim();\n\tconst start = Number.parseInt(match[2], 10);\n\tconst end = Number.parseInt(match[3] ?? match[2], 10);\n\tconst lineCount = countLines(path);\n\tconst valid =\n\t\tfileExists(path) &&\n\t\tNumber.isFinite(start) &&\n\t\tNumber.isFinite(end) &&\n\t\tstart >= 1 &&\n\t\tend >= start &&\n\t\t(lineCount === undefined || end <= lineCount);\n\treturn { path, start, end, valid };\n}\n\nfunction clampTurns(maxTurns: number): number {\n\tif (!Number.isFinite(maxTurns)) return MAX_TURNS_DEFAULT;\n\treturn Math.min(MAX_TURNS_HARD_CAP, Math.max(1, Math.trunc(maxTurns)));\n}\n\nfunction emptyResult(): ScoutRunResult {\n\treturn { summary: \"\", citations: [], droppedCitations: 0, unreliable: false, truncated: false, turnsUsed: 0 };\n}\n\nfunction hasFinalAnswer(text: string): boolean {\n\treturn /<final_answer>[\\s\\S]*?<\\/final_answer>/i.test(text);\n}\n\nfunction assistantText(message: { content: unknown }): string {\n\tif (!Array.isArray(message.content)) return \"\";\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } =>\n\t\t\tBoolean(\n\t\t\t\tcontent &&\n\t\t\t\t\ttypeof content === \"object\" &&\n\t\t\t\t\t(content as { type?: unknown }).type === \"text\" &&\n\t\t\t\t\ttypeof (content as { text?: unknown }).text === \"string\",\n\t\t\t),\n\t\t)\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n"]}
@@ -62,7 +62,7 @@ export class ScoutController {
62
62
  }
63
63
  turnsUsed += 1;
64
64
  lastAssistantText = assistantText(event.message);
65
- outputTokens += event.message.usage?.totalTokens ?? 0;
65
+ outputTokens += getScoutOutputTokenCount(event.message);
66
66
  this.deps.onEvent?.({ type: "scout_turn", detail: `turn ${turnsUsed}` });
67
67
  if (hasFinalAnswer(lastAssistantText)) {
68
68
  return;
@@ -101,6 +101,9 @@ export class ScoutController {
101
101
  return finalResult;
102
102
  }
103
103
  }
104
+ export function getScoutOutputTokenCount(message) {
105
+ return message.usage?.output ?? 0;
106
+ }
104
107
  export function parseScoutAnswer(text, fileExists, countLines) {
105
108
  const finalAnswer = /<final_answer>([\s\S]*?)<\/final_answer>/i.exec(text)?.[1] ?? text;
106
109
  const summaryLines = [];
@@ -1 +1 @@
1
- {"version":3,"file":"scout-controller.js","sourceRoot":"","sources":["../../src/core/scout-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, type AgentTool, type StreamFn } from \"@caupulican/pi-agent-core\";\nimport type { Model } from \"@caupulican/pi-ai\";\n\nexport const SCOUT_SYSTEM_PROMPT = `You are a repository scout. You explore a codebase with read-only tools and return compact evidence. You do NOT solve tasks, write code, or modify anything.\n\nTOOLS: read (file contents), grep (search file contents), find (locate files by glob/name).\n- Issue INDEPENDENT tool calls in parallel in the same turn whenever possible.\n- Prefer grep/find to narrow, then read only the decisive regions.\n\nOUTPUT CONTRACT: when you have enough evidence (or hit your turn budget), reply with:\n<final_answer>\n<one-to-three-sentence summary of what you found>\npath/to/file.ts:START-END\npath/to/other.ts:START-END\n</final_answer>\n- Every path MUST be one you actually observed via a tool result this run.\n- Line ranges point at the decisive code, not whole files.\n- If you found nothing relevant, say so inside <final_answer> with zero citations.\nTurn budget: {MAX_TURNS} turns. Be fast; be precise; cite, don't paste.`;\n\nexport interface ScoutCitation {\n\tpath: string;\n\tstart: number;\n\tend: number;\n\tvalid: boolean;\n}\n\nexport interface ScoutRunResult {\n\tsummary: string;\n\tcitations: ScoutCitation[];\n\tdroppedCitations: number;\n\tunreliable: boolean;\n\ttruncated: boolean;\n\tturnsUsed: number;\n\tfailure?: string;\n}\n\nexport interface ScoutControllerDeps {\n\tresolveScoutModel(): Promise<\n\t\t{ model: Model<any>; apiKey?: string; headers?: Record<string, string> } | { failure: string }\n\t>;\n\tgetCwd(): string;\n\tbuildReadOnlyTools(cwd: string): AgentTool<any>[];\n\tstreamFn?: StreamFn;\n\tfileExists(path: string): boolean;\n\tcountLines(path: string): number | undefined;\n\tonEvent?(event: { type: \"scout_turn\" | \"scout_end\"; detail: string }): void;\n\tsignal?: AbortSignal;\n}\n\nconst MAX_TURNS_DEFAULT = 8;\nconst MAX_TURNS_HARD_CAP = 12;\nconst MAX_OUTPUT_TOKENS = 30_000;\nconst READ_ONLY_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\"]);\n\nexport class ScoutController {\n\tprivate readonly deps: ScoutControllerDeps;\n\n\tconstructor(deps: ScoutControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync run(query: string, maxTurns = MAX_TURNS_DEFAULT): Promise<ScoutRunResult> {\n\t\tconst turnLimit = clampTurns(maxTurns);\n\t\tconst unavailable = emptyResult();\n\t\tif (this.deps.signal?.aborted) {\n\t\t\treturn { ...unavailable, failure: \"aborted\" };\n\t\t}\n\n\t\tlet modelResolution: Awaited<ReturnType<ScoutControllerDeps[\"resolveScoutModel\"]>>;\n\t\ttry {\n\t\t\tmodelResolution = await this.deps.resolveScoutModel();\n\t\t} catch (error) {\n\t\t\treturn { ...unavailable, failure: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t\tif (\"failure\" in modelResolution) {\n\t\t\treturn { ...unavailable, failure: modelResolution.failure };\n\t\t}\n\n\t\tlet turnsUsed = 0;\n\t\tlet truncated = false;\n\t\tlet lastAssistantText = \"\";\n\t\tlet outputTokens = 0;\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tmodel: modelResolution.model,\n\t\t\t\tsystemPrompt: SCOUT_SYSTEM_PROMPT.replace(\"{MAX_TURNS}\", String(turnLimit)),\n\t\t\t\ttools: this.deps\n\t\t\t\t\t.buildReadOnlyTools(this.deps.getCwd())\n\t\t\t\t\t.filter((tool) => READ_ONLY_TOOL_NAMES.has(tool.name)),\n\t\t\t},\n\t\t\tstreamFn: this.deps.streamFn,\n\t\t\tgetApiKey: () => modelResolution.apiKey,\n\t\t\tmaxStallTurns: turnLimit,\n\t\t});\n\n\t\tconst unsubscribe = agent.subscribe((event) => {\n\t\t\tif (event.type !== \"message_end\" || event.message.role !== \"assistant\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tturnsUsed += 1;\n\t\t\tlastAssistantText = assistantText(event.message);\n\t\t\toutputTokens += event.message.usage?.totalTokens ?? 0;\n\t\t\tthis.deps.onEvent?.({ type: \"scout_turn\", detail: `turn ${turnsUsed}` });\n\t\t\tif (hasFinalAnswer(lastAssistantText)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (turnsUsed >= turnLimit || outputTokens >= MAX_OUTPUT_TOKENS) {\n\t\t\t\ttruncated = true;\n\t\t\t\tagent.abort();\n\t\t\t}\n\t\t});\n\n\t\tconst abortScout = (): void => {\n\t\t\ttruncated = true;\n\t\t\tagent.abort();\n\t\t};\n\t\tthis.deps.signal?.addEventListener(\"abort\", abortScout, { once: true });\n\t\tlet runFailure: string | undefined;\n\t\ttry {\n\t\t\tawait agent.prompt(query);\n\t\t} catch (error) {\n\t\t\tif (!truncated && !this.deps.signal?.aborted) {\n\t\t\t\trunFailure = error instanceof Error ? error.message : String(error);\n\t\t\t}\n\t\t} finally {\n\t\t\tunsubscribe();\n\t\t\tthis.deps.signal?.removeEventListener(\"abort\", abortScout);\n\t\t}\n\n\t\tconst result = parseScoutAnswer(\n\t\t\tlastAssistantText,\n\t\t\t(path) => this.deps.fileExists(path),\n\t\t\t(path) => this.deps.countLines(path),\n\t\t);\n\t\tconst finalResult = {\n\t\t\t...result,\n\t\t\ttruncated: truncated || !hasFinalAnswer(lastAssistantText),\n\t\t\tturnsUsed,\n\t\t\tfailure: this.deps.signal?.aborted ? \"aborted\" : runFailure,\n\t\t};\n\t\tthis.deps.onEvent?.({ type: \"scout_end\", detail: finalResult.failure ?? \"ok\" });\n\t\treturn finalResult;\n\t}\n}\n\nexport function parseScoutAnswer(\n\ttext: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): Omit<ScoutRunResult, \"truncated\" | \"turnsUsed\" | \"failure\"> {\n\tconst finalAnswer = /<final_answer>([\\s\\S]*?)<\\/final_answer>/i.exec(text)?.[1] ?? text;\n\tconst summaryLines: string[] = [];\n\tconst citations: ScoutCitation[] = [];\n\n\tfor (const rawLine of finalAnswer.split(/\\r?\\n/)) {\n\t\tconst line = rawLine.trim();\n\t\tif (!line) continue;\n\t\tconst citation = parseCitationLine(line, fileExists, countLines);\n\t\tif (citation) {\n\t\t\tcitations.push(citation);\n\t\t\tcontinue;\n\t\t}\n\t\tsummaryLines.push(line);\n\t}\n\n\tconst droppedCitations = citations.filter((citation) => !citation.valid).length;\n\treturn {\n\t\tsummary: summaryLines.join(\"\\n\"),\n\t\tcitations,\n\t\tdroppedCitations,\n\t\tunreliable: citations.length > 0 && droppedCitations / citations.length > 0.5,\n\t};\n}\n\nfunction parseCitationLine(\n\tline: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): ScoutCitation | undefined {\n\tconst match = /^(.+?):(\\d+)(?:-(\\d+))?$/.exec(line);\n\tif (!match) return undefined;\n\tconst path = match[1].trim();\n\tconst start = Number.parseInt(match[2], 10);\n\tconst end = Number.parseInt(match[3] ?? match[2], 10);\n\tconst lineCount = countLines(path);\n\tconst valid =\n\t\tfileExists(path) &&\n\t\tNumber.isFinite(start) &&\n\t\tNumber.isFinite(end) &&\n\t\tstart >= 1 &&\n\t\tend >= start &&\n\t\t(lineCount === undefined || end <= lineCount);\n\treturn { path, start, end, valid };\n}\n\nfunction clampTurns(maxTurns: number): number {\n\tif (!Number.isFinite(maxTurns)) return MAX_TURNS_DEFAULT;\n\treturn Math.min(MAX_TURNS_HARD_CAP, Math.max(1, Math.trunc(maxTurns)));\n}\n\nfunction emptyResult(): ScoutRunResult {\n\treturn { summary: \"\", citations: [], droppedCitations: 0, unreliable: false, truncated: false, turnsUsed: 0 };\n}\n\nfunction hasFinalAnswer(text: string): boolean {\n\treturn /<final_answer>[\\s\\S]*?<\\/final_answer>/i.test(text);\n}\n\nfunction assistantText(message: { content: unknown }): string {\n\tif (!Array.isArray(message.content)) return \"\";\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } =>\n\t\t\tBoolean(\n\t\t\t\tcontent &&\n\t\t\t\t\ttypeof content === \"object\" &&\n\t\t\t\t\t(content as { type?: unknown }).type === \"text\" &&\n\t\t\t\t\ttypeof (content as { text?: unknown }).text === \"string\",\n\t\t\t),\n\t\t)\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"scout-controller.js","sourceRoot":"","sources":["../../src/core/scout-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, type AgentTool, type StreamFn } from \"@caupulican/pi-agent-core\";\nimport type { Model } from \"@caupulican/pi-ai\";\n\nexport const SCOUT_SYSTEM_PROMPT = `You are a repository scout. You explore a codebase with read-only tools and return compact evidence. You do NOT solve tasks, write code, or modify anything.\n\nTOOLS: read (file contents), grep (search file contents), find (locate files by glob/name).\n- Issue INDEPENDENT tool calls in parallel in the same turn whenever possible.\n- Prefer grep/find to narrow, then read only the decisive regions.\n\nOUTPUT CONTRACT: when you have enough evidence (or hit your turn budget), reply with:\n<final_answer>\n<one-to-three-sentence summary of what you found>\npath/to/file.ts:START-END\npath/to/other.ts:START-END\n</final_answer>\n- Every path MUST be one you actually observed via a tool result this run.\n- Line ranges point at the decisive code, not whole files.\n- If you found nothing relevant, say so inside <final_answer> with zero citations.\nTurn budget: {MAX_TURNS} turns. Be fast; be precise; cite, don't paste.`;\n\nexport interface ScoutCitation {\n\tpath: string;\n\tstart: number;\n\tend: number;\n\tvalid: boolean;\n}\n\nexport interface ScoutRunResult {\n\tsummary: string;\n\tcitations: ScoutCitation[];\n\tdroppedCitations: number;\n\tunreliable: boolean;\n\ttruncated: boolean;\n\tturnsUsed: number;\n\tfailure?: string;\n}\n\nexport interface ScoutControllerDeps {\n\tresolveScoutModel(): Promise<\n\t\t{ model: Model<any>; apiKey?: string; headers?: Record<string, string> } | { failure: string }\n\t>;\n\tgetCwd(): string;\n\tbuildReadOnlyTools(cwd: string): AgentTool<any>[];\n\tstreamFn?: StreamFn;\n\tfileExists(path: string): boolean;\n\tcountLines(path: string): number | undefined;\n\tonEvent?(event: { type: \"scout_turn\" | \"scout_end\"; detail: string }): void;\n\tsignal?: AbortSignal;\n}\n\nconst MAX_TURNS_DEFAULT = 8;\nconst MAX_TURNS_HARD_CAP = 12;\nconst MAX_OUTPUT_TOKENS = 30_000;\nconst READ_ONLY_TOOL_NAMES = new Set([\"read\", \"grep\", \"find\"]);\n\nexport class ScoutController {\n\tprivate readonly deps: ScoutControllerDeps;\n\n\tconstructor(deps: ScoutControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\tasync run(query: string, maxTurns = MAX_TURNS_DEFAULT): Promise<ScoutRunResult> {\n\t\tconst turnLimit = clampTurns(maxTurns);\n\t\tconst unavailable = emptyResult();\n\t\tif (this.deps.signal?.aborted) {\n\t\t\treturn { ...unavailable, failure: \"aborted\" };\n\t\t}\n\n\t\tlet modelResolution: Awaited<ReturnType<ScoutControllerDeps[\"resolveScoutModel\"]>>;\n\t\ttry {\n\t\t\tmodelResolution = await this.deps.resolveScoutModel();\n\t\t} catch (error) {\n\t\t\treturn { ...unavailable, failure: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t\tif (\"failure\" in modelResolution) {\n\t\t\treturn { ...unavailable, failure: modelResolution.failure };\n\t\t}\n\n\t\tlet turnsUsed = 0;\n\t\tlet truncated = false;\n\t\tlet lastAssistantText = \"\";\n\t\tlet outputTokens = 0;\n\t\tconst agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tmodel: modelResolution.model,\n\t\t\t\tsystemPrompt: SCOUT_SYSTEM_PROMPT.replace(\"{MAX_TURNS}\", String(turnLimit)),\n\t\t\t\ttools: this.deps\n\t\t\t\t\t.buildReadOnlyTools(this.deps.getCwd())\n\t\t\t\t\t.filter((tool) => READ_ONLY_TOOL_NAMES.has(tool.name)),\n\t\t\t},\n\t\t\tstreamFn: this.deps.streamFn,\n\t\t\tgetApiKey: () => modelResolution.apiKey,\n\t\t\tmaxStallTurns: turnLimit,\n\t\t});\n\n\t\tconst unsubscribe = agent.subscribe((event) => {\n\t\t\tif (event.type !== \"message_end\" || event.message.role !== \"assistant\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tturnsUsed += 1;\n\t\t\tlastAssistantText = assistantText(event.message);\n\t\t\toutputTokens += getScoutOutputTokenCount(event.message);\n\t\t\tthis.deps.onEvent?.({ type: \"scout_turn\", detail: `turn ${turnsUsed}` });\n\t\t\tif (hasFinalAnswer(lastAssistantText)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (turnsUsed >= turnLimit || outputTokens >= MAX_OUTPUT_TOKENS) {\n\t\t\t\ttruncated = true;\n\t\t\t\tagent.abort();\n\t\t\t}\n\t\t});\n\n\t\tconst abortScout = (): void => {\n\t\t\ttruncated = true;\n\t\t\tagent.abort();\n\t\t};\n\t\tthis.deps.signal?.addEventListener(\"abort\", abortScout, { once: true });\n\t\tlet runFailure: string | undefined;\n\t\ttry {\n\t\t\tawait agent.prompt(query);\n\t\t} catch (error) {\n\t\t\tif (!truncated && !this.deps.signal?.aborted) {\n\t\t\t\trunFailure = error instanceof Error ? error.message : String(error);\n\t\t\t}\n\t\t} finally {\n\t\t\tunsubscribe();\n\t\t\tthis.deps.signal?.removeEventListener(\"abort\", abortScout);\n\t\t}\n\n\t\tconst result = parseScoutAnswer(\n\t\t\tlastAssistantText,\n\t\t\t(path) => this.deps.fileExists(path),\n\t\t\t(path) => this.deps.countLines(path),\n\t\t);\n\t\tconst finalResult = {\n\t\t\t...result,\n\t\t\ttruncated: truncated || !hasFinalAnswer(lastAssistantText),\n\t\t\tturnsUsed,\n\t\t\tfailure: this.deps.signal?.aborted ? \"aborted\" : runFailure,\n\t\t};\n\t\tthis.deps.onEvent?.({ type: \"scout_end\", detail: finalResult.failure ?? \"ok\" });\n\t\treturn finalResult;\n\t}\n}\n\nexport function getScoutOutputTokenCount(message: { usage?: { output?: number } }): number {\n\treturn message.usage?.output ?? 0;\n}\n\nexport function parseScoutAnswer(\n\ttext: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): Omit<ScoutRunResult, \"truncated\" | \"turnsUsed\" | \"failure\"> {\n\tconst finalAnswer = /<final_answer>([\\s\\S]*?)<\\/final_answer>/i.exec(text)?.[1] ?? text;\n\tconst summaryLines: string[] = [];\n\tconst citations: ScoutCitation[] = [];\n\n\tfor (const rawLine of finalAnswer.split(/\\r?\\n/)) {\n\t\tconst line = rawLine.trim();\n\t\tif (!line) continue;\n\t\tconst citation = parseCitationLine(line, fileExists, countLines);\n\t\tif (citation) {\n\t\t\tcitations.push(citation);\n\t\t\tcontinue;\n\t\t}\n\t\tsummaryLines.push(line);\n\t}\n\n\tconst droppedCitations = citations.filter((citation) => !citation.valid).length;\n\treturn {\n\t\tsummary: summaryLines.join(\"\\n\"),\n\t\tcitations,\n\t\tdroppedCitations,\n\t\tunreliable: citations.length > 0 && droppedCitations / citations.length > 0.5,\n\t};\n}\n\nfunction parseCitationLine(\n\tline: string,\n\tfileExists: (path: string) => boolean,\n\tcountLines: (path: string) => number | undefined,\n): ScoutCitation | undefined {\n\tconst match = /^(.+?):(\\d+)(?:-(\\d+))?$/.exec(line);\n\tif (!match) return undefined;\n\tconst path = match[1].trim();\n\tconst start = Number.parseInt(match[2], 10);\n\tconst end = Number.parseInt(match[3] ?? match[2], 10);\n\tconst lineCount = countLines(path);\n\tconst valid =\n\t\tfileExists(path) &&\n\t\tNumber.isFinite(start) &&\n\t\tNumber.isFinite(end) &&\n\t\tstart >= 1 &&\n\t\tend >= start &&\n\t\t(lineCount === undefined || end <= lineCount);\n\treturn { path, start, end, valid };\n}\n\nfunction clampTurns(maxTurns: number): number {\n\tif (!Number.isFinite(maxTurns)) return MAX_TURNS_DEFAULT;\n\treturn Math.min(MAX_TURNS_HARD_CAP, Math.max(1, Math.trunc(maxTurns)));\n}\n\nfunction emptyResult(): ScoutRunResult {\n\treturn { summary: \"\", citations: [], droppedCitations: 0, unreliable: false, truncated: false, turnsUsed: 0 };\n}\n\nfunction hasFinalAnswer(text: string): boolean {\n\treturn /<final_answer>[\\s\\S]*?<\\/final_answer>/i.test(text);\n}\n\nfunction assistantText(message: { content: unknown }): string {\n\tif (!Array.isArray(message.content)) return \"\";\n\treturn message.content\n\t\t.filter((content): content is { type: \"text\"; text: string } =>\n\t\t\tBoolean(\n\t\t\t\tcontent &&\n\t\t\t\t\ttypeof content === \"object\" &&\n\t\t\t\t\t(content as { type?: unknown }).type === \"text\" &&\n\t\t\t\t\ttypeof (content as { text?: unknown }).text === \"string\",\n\t\t\t),\n\t\t)\n\t\t.map((content) => content.text)\n\t\t.join(\"\\n\");\n}\n"]}