@animus-labs/cortex 0.2.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF text extraction.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `unpdf` (pure-ESM, zero native deps) behind a narrow, well-typed
|
|
5
|
+
* boundary so the Read tool never touches pdfjs directly. Swapping the
|
|
6
|
+
* backend later is a one-file change.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Parse the caller's `pages` spec and clamp it to the document and
|
|
10
|
+
* the per-call page cap.
|
|
11
|
+
* - Extract per-page text.
|
|
12
|
+
* - Detect "no extractable text" (scanned / image-only PDFs) and
|
|
13
|
+
* return a structured signal rather than silently-empty output.
|
|
14
|
+
* - Render the extracted text with `[Page N]` markers so the caller
|
|
15
|
+
* can line-number it exactly like any other file content.
|
|
16
|
+
*
|
|
17
|
+
* Pure-ish: does no filesystem I/O. Callers are expected to have
|
|
18
|
+
* already loaded the PDF bytes.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export interface PdfExtractionRequest {
|
|
26
|
+
/** PDF bytes. Accepts a Node Buffer or any Uint8Array. */
|
|
27
|
+
data: Buffer | Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Page spec: `"N"`, `"N-M"`, or undefined. When undefined, extracts
|
|
30
|
+
* the first `maxPages` pages starting at page 1.
|
|
31
|
+
*/
|
|
32
|
+
pagesSpec?: string | undefined;
|
|
33
|
+
/** Upper bound on how many pages may be extracted in one call. */
|
|
34
|
+
maxPages?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PdfExtractionOk {
|
|
38
|
+
kind: 'ok';
|
|
39
|
+
totalPages: number;
|
|
40
|
+
/** First page extracted (1-based, inclusive). */
|
|
41
|
+
firstPage: number;
|
|
42
|
+
/** Last page extracted (1-based, inclusive). */
|
|
43
|
+
lastPage: number;
|
|
44
|
+
/** Per-page text. `pages[i].pageNumber` is 1-based. */
|
|
45
|
+
pages: Array<{ pageNumber: number; text: string }>;
|
|
46
|
+
/**
|
|
47
|
+
* Full rendered text with `[Page N]` markers separating pages, ready
|
|
48
|
+
* to be handed to the same line-numbering pipeline Read uses for
|
|
49
|
+
* plain text files.
|
|
50
|
+
*/
|
|
51
|
+
rendered: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PdfExtractionEmpty {
|
|
55
|
+
kind: 'empty';
|
|
56
|
+
totalPages: number;
|
|
57
|
+
firstPage: number;
|
|
58
|
+
lastPage: number;
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PdfExtractionInvalidRange {
|
|
63
|
+
kind: 'invalid-range';
|
|
64
|
+
totalPages: number;
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PdfExtractionError {
|
|
69
|
+
kind: 'error';
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type PdfExtractionResult =
|
|
74
|
+
| PdfExtractionOk
|
|
75
|
+
| PdfExtractionEmpty
|
|
76
|
+
| PdfExtractionInvalidRange
|
|
77
|
+
| PdfExtractionError;
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Constants
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Default cap on pages extracted per call. Matches the advertised
|
|
85
|
+
* schema default for Read's `pages` param ("max 20 pages per request").
|
|
86
|
+
*/
|
|
87
|
+
export const DEFAULT_MAX_PAGES = 20;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Total-text length below which we treat the document as image-based.
|
|
91
|
+
* 20 characters allows for a stray page number or watermark without
|
|
92
|
+
* pretending we extracted meaningful content.
|
|
93
|
+
*/
|
|
94
|
+
const EMPTY_TEXT_THRESHOLD = 20;
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Pages-spec parsing (pure, unit-testable)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
export type PagesSpecResult =
|
|
101
|
+
| { ok: true; first: number; last: number }
|
|
102
|
+
| { ok: false; message: string };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parse a pages-spec string against a document's page count and the
|
|
106
|
+
* per-call cap. Enforces:
|
|
107
|
+
* - Format: `"N"` or `"N-M"` (base 10, whitespace tolerant)
|
|
108
|
+
* - 1 <= first <= last <= totalPages
|
|
109
|
+
* - (last - first + 1) <= maxPages
|
|
110
|
+
*
|
|
111
|
+
* Returns `{ ok: false }` with an actionable message on any failure.
|
|
112
|
+
*/
|
|
113
|
+
export function parsePagesSpec(
|
|
114
|
+
spec: string,
|
|
115
|
+
totalPages: number,
|
|
116
|
+
maxPages: number,
|
|
117
|
+
): PagesSpecResult {
|
|
118
|
+
const trimmed = spec.trim();
|
|
119
|
+
const rangeMatch = /^(\d+)-(\d+)$/u.exec(trimmed);
|
|
120
|
+
const singleMatch = /^(\d+)$/u.exec(trimmed);
|
|
121
|
+
|
|
122
|
+
let first: number;
|
|
123
|
+
let last: number;
|
|
124
|
+
if (rangeMatch) {
|
|
125
|
+
first = Number.parseInt(rangeMatch[1]!, 10);
|
|
126
|
+
last = Number.parseInt(rangeMatch[2]!, 10);
|
|
127
|
+
} else if (singleMatch) {
|
|
128
|
+
first = Number.parseInt(singleMatch[1]!, 10);
|
|
129
|
+
last = first;
|
|
130
|
+
} else {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
message: `Invalid pages spec "${spec}". Use "N" or "N-M" (e.g. "1-5", "3").`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (first < 1) {
|
|
138
|
+
return { ok: false, message: `Page numbers are 1-based; got first page ${first}.` };
|
|
139
|
+
}
|
|
140
|
+
if (last < first) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
message: `Invalid pages spec "${spec}": last page (${last}) is before first page (${first}).`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (last > totalPages) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
message: `Pages spec "${spec}" exceeds document (has ${totalPages} page${totalPages === 1 ? '' : 's'}).`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const count = last - first + 1;
|
|
153
|
+
if (count > maxPages) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
message: `Pages spec "${spec}" requests ${count} pages; the per-call limit is ${maxPages}. Narrow the range.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { ok: true, first, last };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Buffer normalization
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Convert input bytes to a fresh, owned `Uint8Array`.
|
|
169
|
+
*
|
|
170
|
+
* `unpdf` rejects `Buffer` inputs outright ("Please provide binary data
|
|
171
|
+
* as `Uint8Array`, rather than `Buffer`"), and its PDF.js worker path
|
|
172
|
+
* may transfer the backing buffer during postMessage — leaving a shared
|
|
173
|
+
* view detached for subsequent calls. Making a full copy here keeps
|
|
174
|
+
* the caller's buffer usable and makes repeat extractions on the same
|
|
175
|
+
* bytes safe across tests and sessions.
|
|
176
|
+
*/
|
|
177
|
+
function toUint8Array(data: Buffer | Uint8Array): Uint8Array {
|
|
178
|
+
return new Uint8Array(data);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Extraction
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extract text from a PDF buffer. Never throws — all failure modes are
|
|
187
|
+
* returned as structured results so the caller can render them as
|
|
188
|
+
* tool-content messages.
|
|
189
|
+
*/
|
|
190
|
+
export async function extractPdfText(
|
|
191
|
+
req: PdfExtractionRequest,
|
|
192
|
+
): Promise<PdfExtractionResult> {
|
|
193
|
+
const maxPages = req.maxPages ?? DEFAULT_MAX_PAGES;
|
|
194
|
+
const bytes = toUint8Array(req.data);
|
|
195
|
+
|
|
196
|
+
let extractResult: { totalPages: number; text: string[] };
|
|
197
|
+
try {
|
|
198
|
+
const { extractText } = await import('unpdf');
|
|
199
|
+
const raw = await extractText(bytes, { mergePages: false });
|
|
200
|
+
extractResult = {
|
|
201
|
+
totalPages: raw.totalPages,
|
|
202
|
+
text: Array.isArray(raw.text) ? raw.text : [raw.text],
|
|
203
|
+
};
|
|
204
|
+
} catch (err: unknown) {
|
|
205
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
206
|
+
return {
|
|
207
|
+
kind: 'error',
|
|
208
|
+
message: `Failed to parse PDF: ${msg}`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const { totalPages, text: allPages } = extractResult;
|
|
213
|
+
|
|
214
|
+
if (totalPages === 0) {
|
|
215
|
+
return {
|
|
216
|
+
kind: 'empty',
|
|
217
|
+
totalPages: 0,
|
|
218
|
+
firstPage: 0,
|
|
219
|
+
lastPage: 0,
|
|
220
|
+
message: 'PDF contains no pages.',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Resolve the page range.
|
|
225
|
+
let first: number;
|
|
226
|
+
let last: number;
|
|
227
|
+
if (req.pagesSpec !== undefined) {
|
|
228
|
+
const parsed = parsePagesSpec(req.pagesSpec, totalPages, maxPages);
|
|
229
|
+
if (!parsed.ok) {
|
|
230
|
+
return { kind: 'invalid-range', totalPages, message: parsed.message };
|
|
231
|
+
}
|
|
232
|
+
first = parsed.first;
|
|
233
|
+
last = parsed.last;
|
|
234
|
+
} else {
|
|
235
|
+
first = 1;
|
|
236
|
+
last = Math.min(maxPages, totalPages);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const pages: Array<{ pageNumber: number; text: string }> = [];
|
|
240
|
+
for (let pageNumber = first; pageNumber <= last; pageNumber++) {
|
|
241
|
+
const raw = allPages[pageNumber - 1] ?? '';
|
|
242
|
+
pages.push({ pageNumber, text: raw });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const totalLen = pages.reduce((n, p) => n + p.text.trim().length, 0);
|
|
246
|
+
if (totalLen < EMPTY_TEXT_THRESHOLD) {
|
|
247
|
+
return {
|
|
248
|
+
kind: 'empty',
|
|
249
|
+
totalPages,
|
|
250
|
+
firstPage: first,
|
|
251
|
+
lastPage: last,
|
|
252
|
+
message:
|
|
253
|
+
totalPages > 0 && totalLen === 0
|
|
254
|
+
? 'PDF has no extractable text (likely scanned or image-only). Use an OCR tool to process it.'
|
|
255
|
+
: 'PDF yielded almost no extractable text (likely scanned or image-heavy). Use an OCR tool for full content.',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const rendered = renderPages(pages, first, last, totalPages);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
kind: 'ok',
|
|
263
|
+
totalPages,
|
|
264
|
+
firstPage: first,
|
|
265
|
+
lastPage: last,
|
|
266
|
+
pages,
|
|
267
|
+
rendered,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Render the per-page text into a single string with `[Page N]`
|
|
273
|
+
* markers. A leading summary line is included so the model knows how
|
|
274
|
+
* many pages the document has and which subset it's seeing.
|
|
275
|
+
*/
|
|
276
|
+
function renderPages(
|
|
277
|
+
pages: Array<{ pageNumber: number; text: string }>,
|
|
278
|
+
firstPage: number,
|
|
279
|
+
lastPage: number,
|
|
280
|
+
totalPages: number,
|
|
281
|
+
): string {
|
|
282
|
+
const header =
|
|
283
|
+
firstPage === 1 && lastPage === totalPages
|
|
284
|
+
? `[PDF: ${totalPages} page${totalPages === 1 ? '' : 's'}]`
|
|
285
|
+
: `[PDF: showing pages ${firstPage}-${lastPage} of ${totalPages}]`;
|
|
286
|
+
const body = pages
|
|
287
|
+
.map((p) => `[Page ${p.pageNumber}]\n${p.text.trim()}`)
|
|
288
|
+
.join('\n\n');
|
|
289
|
+
return `${header}\n\n${body}\n`;
|
|
290
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop-scoped file read tracking.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the Read, Write, and Edit tools to enforce
|
|
5
|
+
* the read-before-write/edit contract. Tracks which files
|
|
6
|
+
* have been read during the current agentic loop, along with
|
|
7
|
+
* metadata (mtime, offset, limit) for file-unchanged dedup.
|
|
8
|
+
*
|
|
9
|
+
* Created once per CortexAgent and cleared at the start
|
|
10
|
+
* of each agentic loop via clear().
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export interface ReadState {
|
|
16
|
+
/** File mtime at time of read (ms since epoch). */
|
|
17
|
+
timestamp: number;
|
|
18
|
+
/** 1-based offset used for the read (undefined = full read). */
|
|
19
|
+
offset?: number;
|
|
20
|
+
/** Line limit used for the read (undefined = default/full). */
|
|
21
|
+
limit?: number;
|
|
22
|
+
/**
|
|
23
|
+
* SHA-256 hex digest of the raw file bytes at the time of read.
|
|
24
|
+
* Populated only for non-truncated, full reads; used as a fallback
|
|
25
|
+
* on mtime mismatch to allow writes when the on-disk bytes are
|
|
26
|
+
* actually unchanged (e.g. a formatter or cloud-sync tool touched
|
|
27
|
+
* the mtime without modifying content).
|
|
28
|
+
*/
|
|
29
|
+
contentHash?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ReadRegistry {
|
|
33
|
+
private readonly entries = new Map<string, ReadState>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mark a file as read with metadata for dedup.
|
|
37
|
+
* The path is normalized to an absolute, platform-canonical form.
|
|
38
|
+
*/
|
|
39
|
+
markRead(filePath: string, state?: ReadState): void {
|
|
40
|
+
this.entries.set(
|
|
41
|
+
this.normalize(filePath),
|
|
42
|
+
state ?? { timestamp: Date.now() },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check whether a file has been read in the current agentic loop.
|
|
48
|
+
*/
|
|
49
|
+
hasBeenRead(filePath: string): boolean {
|
|
50
|
+
return this.entries.has(this.normalize(filePath));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the read state for a file, or undefined if not read.
|
|
55
|
+
*/
|
|
56
|
+
getState(filePath: string): ReadState | undefined {
|
|
57
|
+
return this.entries.get(this.normalize(filePath));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Invalidate a single file's read state.
|
|
62
|
+
* Called when the on-disk mtime diverges from the recorded read
|
|
63
|
+
* state (external modification), forcing a fresh Read before
|
|
64
|
+
* the next mutation. Successful Edit/Write calls instead call
|
|
65
|
+
* markRead() with the new mtime, since the agent's own mutation
|
|
66
|
+
* is authoritative knowledge of current file contents.
|
|
67
|
+
*/
|
|
68
|
+
invalidate(filePath: string): void {
|
|
69
|
+
this.entries.delete(this.normalize(filePath));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clear all read tracking. Called at the start of each agentic loop.
|
|
74
|
+
*/
|
|
75
|
+
clear(): void {
|
|
76
|
+
this.entries.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the number of tracked files (for diagnostics).
|
|
81
|
+
*/
|
|
82
|
+
get size(): number {
|
|
83
|
+
return this.entries.size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Normalize a file path for consistent comparison.
|
|
88
|
+
* Resolves to absolute and normalizes separators.
|
|
89
|
+
*/
|
|
90
|
+
private normalize(filePath: string): string {
|
|
91
|
+
return path.resolve(filePath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared environment sanitization for child processes.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the Bash tool (safety.ts) and MCP client (mcp-client.ts)
|
|
5
|
+
* to strip dangerous environment variables before spawning subprocesses.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Blocked variables
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const BLOCKED_ENV_PREFIXES = ['LD_', 'DYLD_', 'BASH_FUNC_'];
|
|
13
|
+
|
|
14
|
+
const BLOCKED_ENV_VARS = new Set([
|
|
15
|
+
// Runtime loaders
|
|
16
|
+
'NODE_OPTIONS', 'NODE_PATH',
|
|
17
|
+
'PYTHONPATH', 'PYTHONHOME',
|
|
18
|
+
'PERL5LIB', 'PERL5OPT',
|
|
19
|
+
'RUBYLIB', 'RUBYOPT',
|
|
20
|
+
// Shell startup injection
|
|
21
|
+
'BASH_ENV', 'ENV', 'SHELLOPTS', 'PS4', 'IFS', 'PROMPT_COMMAND', 'ZDOTDIR',
|
|
22
|
+
// Git execution
|
|
23
|
+
'GIT_EXTERNAL_DIFF', 'GIT_EXEC_PATH', 'GIT_SSH_COMMAND',
|
|
24
|
+
// Security-sensitive
|
|
25
|
+
'SSLKEYLOGFILE', 'GCONV_PATH', 'OPENSSL_CONF', 'CURL_HOME', 'WGETRC',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Public API
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a safe environment for child processes by stripping dangerous variables.
|
|
34
|
+
*
|
|
35
|
+
* @param parentEnv - The source environment (typically process.env or a consumer-supplied map)
|
|
36
|
+
* @param marker - Optional context marker added as CORTEX_SHELL. Pass undefined to skip.
|
|
37
|
+
* @param overrides - Optional key-value pairs merged ON TOP of the sanitized env, bypassing
|
|
38
|
+
* the blocklist. Used for consumer-set variables that must propagate (e.g., macOS dock
|
|
39
|
+
* icon suppression vars like DYLD_INSERT_LIBRARIES).
|
|
40
|
+
* @returns A new object with dangerous variables removed and overrides applied
|
|
41
|
+
*/
|
|
42
|
+
export function buildSafeEnv(
|
|
43
|
+
parentEnv: NodeJS.ProcessEnv | Record<string, string>,
|
|
44
|
+
marker?: string | undefined,
|
|
45
|
+
overrides?: Record<string, string> | undefined,
|
|
46
|
+
): Record<string, string> {
|
|
47
|
+
const env: Record<string, string> = {};
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(parentEnv)) {
|
|
50
|
+
if (value === undefined) continue;
|
|
51
|
+
|
|
52
|
+
// Check exact match
|
|
53
|
+
if (BLOCKED_ENV_VARS.has(key)) continue;
|
|
54
|
+
|
|
55
|
+
// Check prefix match
|
|
56
|
+
let blocked = false;
|
|
57
|
+
for (const prefix of BLOCKED_ENV_PREFIXES) {
|
|
58
|
+
if (key.startsWith(prefix)) {
|
|
59
|
+
blocked = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (blocked) continue;
|
|
64
|
+
|
|
65
|
+
env[key] = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (marker !== undefined) {
|
|
69
|
+
env['CORTEX_SHELL'] = marker;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Merge overrides ON TOP of the sanitized env, bypassing the blocklist.
|
|
73
|
+
// This allows consumers to restore specific blocked variables (e.g.,
|
|
74
|
+
// DYLD_INSERT_LIBRARIES for macOS dock icon suppression).
|
|
75
|
+
if (overrides) {
|
|
76
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
77
|
+
env[key] = value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return env;
|
|
82
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubAgent tool: spawn independent cortex-based sub-agents for delegated work.
|
|
3
|
+
*
|
|
4
|
+
* Supports foreground (blocking) and background (async) execution modes.
|
|
5
|
+
* Each sub-agent is an independent CortexAgent with its own message array
|
|
6
|
+
* and empty context slots.
|
|
7
|
+
*
|
|
8
|
+
* The SubAgent tool is ALWAYS excluded from child agents to prevent
|
|
9
|
+
* recursive spawning.
|
|
10
|
+
*
|
|
11
|
+
* References:
|
|
12
|
+
* - docs/cortex/tools/sub-agent.md
|
|
13
|
+
* - docs/cortex/plans/phase-4-sub-agents-and-skills.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Type, type Static } from 'typebox';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Schema
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export const SubAgentParams = Type.Object({
|
|
23
|
+
instructions: Type.String({
|
|
24
|
+
description: 'What the sub-agent should do. This becomes the sub-agent\'s initial prompt.',
|
|
25
|
+
}),
|
|
26
|
+
tools: Type.Optional(Type.Array(Type.String(), {
|
|
27
|
+
description: 'Tool names to make available. Default: inherits parent\'s registered tools.',
|
|
28
|
+
})),
|
|
29
|
+
systemPrompt: Type.Optional(Type.String({
|
|
30
|
+
description: 'Custom system prompt. Default: inherits parent\'s full system prompt.',
|
|
31
|
+
})),
|
|
32
|
+
maxTurns: Type.Optional(Type.Number({
|
|
33
|
+
description: 'Maximum LLM turns. Default: inherits parent\'s budget guard config.',
|
|
34
|
+
})),
|
|
35
|
+
maxCost: Type.Optional(Type.Number({
|
|
36
|
+
description: 'Maximum cost in USD. Default: inherits parent\'s budget guard config.',
|
|
37
|
+
})),
|
|
38
|
+
background: Type.Optional(Type.Boolean({
|
|
39
|
+
description: 'Run asynchronously. Default: false (blocks until complete).',
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type SubAgentParamsType = Static<typeof SubAgentParams>;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Details type (for UI/logs)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export interface SubAgentDetails {
|
|
50
|
+
taskId: string;
|
|
51
|
+
background: boolean;
|
|
52
|
+
status: string;
|
|
53
|
+
durationMs: number | null;
|
|
54
|
+
turns: number | null;
|
|
55
|
+
cost: number | null;
|
|
56
|
+
/** Model ID used by the sub-agent (inherited from parent). */
|
|
57
|
+
modelId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Config
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configuration passed to the SubAgent tool factory.
|
|
66
|
+
* The CortexAgent provides all of these at tool registration time.
|
|
67
|
+
*/
|
|
68
|
+
export interface SubAgentToolConfig {
|
|
69
|
+
/**
|
|
70
|
+
* Spawn a sub-agent and run it. Returns the result when complete.
|
|
71
|
+
* The factory function handles CortexAgent creation, budget guard
|
|
72
|
+
* inheritance, tool filtering, and lifecycle management.
|
|
73
|
+
*/
|
|
74
|
+
spawnSubAgent: (params: SubAgentParamsType) => Promise<{
|
|
75
|
+
taskId: string;
|
|
76
|
+
output: string;
|
|
77
|
+
status: string;
|
|
78
|
+
usage: { turns: number; cost: number; durationMs: number };
|
|
79
|
+
}>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Spawn a background sub-agent. Returns the task ID immediately.
|
|
83
|
+
*/
|
|
84
|
+
spawnBackgroundSubAgent: (params: SubAgentParamsType) => Promise<{
|
|
85
|
+
taskId: string;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if another sub-agent can be spawned.
|
|
90
|
+
*/
|
|
91
|
+
canSpawn: () => boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get concurrency info for error messages.
|
|
95
|
+
*/
|
|
96
|
+
getConcurrencyInfo: () => { active: number; limit: number };
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the model ID for the child agent.
|
|
100
|
+
* Child agents inherit the parent's primary model.
|
|
101
|
+
*/
|
|
102
|
+
getModelId: () => string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Tool name constant
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export const SUB_AGENT_TOOL_NAME = 'SubAgent';
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Factory
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create the SubAgent tool.
|
|
117
|
+
*
|
|
118
|
+
* Returns a Cortex-native tool. CortexAgent adapts it to pi-agent-core's
|
|
119
|
+
* execute signature when synchronizing the tool inventory.
|
|
120
|
+
*/
|
|
121
|
+
export function createSubAgentTool(config: SubAgentToolConfig): {
|
|
122
|
+
name: string;
|
|
123
|
+
description: string;
|
|
124
|
+
parameters: typeof SubAgentParams;
|
|
125
|
+
execute: (args: unknown) => Promise<unknown>;
|
|
126
|
+
} {
|
|
127
|
+
return {
|
|
128
|
+
name: SUB_AGENT_TOOL_NAME,
|
|
129
|
+
description: `Spawn a sub-agent to handle a delegated task independently. Use for tasks that are complex, long-running, or can proceed in parallel with your main work.
|
|
130
|
+
|
|
131
|
+
Foreground mode (default): Blocks until the sub-agent completes and returns its result directly. Use for quick, focused tasks where you need the result to continue.
|
|
132
|
+
|
|
133
|
+
Background mode (background: true): Returns a task ID immediately. The sub-agent runs independently. You will be notified when it completes. Use for long-running research, analysis, or multi-step work.
|
|
134
|
+
|
|
135
|
+
Sub-agents are independent: they have their own conversation, do not share your context, and cannot spawn further sub-agents. Give them clear, self-contained instructions.`,
|
|
136
|
+
|
|
137
|
+
parameters: SubAgentParams,
|
|
138
|
+
|
|
139
|
+
execute: async (args: unknown): Promise<unknown> => {
|
|
140
|
+
const params = args as SubAgentParamsType;
|
|
141
|
+
|
|
142
|
+
// Check concurrency limit
|
|
143
|
+
if (!config.canSpawn()) {
|
|
144
|
+
const info = config.getConcurrencyInfo();
|
|
145
|
+
return `Cannot spawn sub-agent: concurrency limit reached (${info.active}/${info.limit} active). Wait for a running sub-agent to complete or cancel one to free a slot.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Background mode: spawn and return immediately
|
|
149
|
+
if (params.background) {
|
|
150
|
+
const { taskId } = await config.spawnBackgroundSubAgent(params);
|
|
151
|
+
return `Sub-agent spawned in background. Task ID: ${taskId}\nYou will be notified when it completes. Continue with other work.`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Foreground mode: block until complete
|
|
155
|
+
const result = await config.spawnSubAgent(params);
|
|
156
|
+
|
|
157
|
+
// Format result for the parent agent
|
|
158
|
+
const statusLine = result.status === 'completed'
|
|
159
|
+
? 'Sub-agent completed successfully.'
|
|
160
|
+
: `Sub-agent finished with status: ${result.status}`;
|
|
161
|
+
|
|
162
|
+
const usageLine = `(${result.usage.turns} turns, $${result.usage.cost.toFixed(4)}, ${(result.usage.durationMs / 1000).toFixed(1)}s)`;
|
|
163
|
+
|
|
164
|
+
if (result.output) {
|
|
165
|
+
return `${statusLine} ${usageLine}\n\n${result.output}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return `${statusLine} ${usageLine}\n\nNo output was produced.`;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|