@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* load_skill tool: loads a skill's full instructions into the agent's
|
|
3
|
+
* active context.
|
|
4
|
+
*
|
|
5
|
+
* The skill body is read from the SkillRegistry, preprocessed (variable
|
|
6
|
+
* substitution, shell commands, scripts), and pushed to the skill buffer.
|
|
7
|
+
* The skill buffer is injected into ephemeral context via transformContext
|
|
8
|
+
* on every subsequent LLM call within the current agentic loop.
|
|
9
|
+
*
|
|
10
|
+
* References:
|
|
11
|
+
* - docs/cortex/skill-system.md
|
|
12
|
+
* - docs/cortex/plans/phase-4-sub-agents-and-skills.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Type, type Static } from 'typebox';
|
|
16
|
+
import type { SkillRegistry } from './skill-registry.js';
|
|
17
|
+
import type { LoadedSkill } from './types.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Schema
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export const LoadSkillParams = Type.Object({
|
|
24
|
+
name: Type.String({
|
|
25
|
+
description: 'The skill name to load.',
|
|
26
|
+
}),
|
|
27
|
+
arguments: Type.Optional(Type.String({
|
|
28
|
+
description: 'Optional arguments to pass to the skill.',
|
|
29
|
+
})),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type LoadSkillParamsType = Static<typeof LoadSkillParams>;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Tool name constant
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export const LOAD_SKILL_TOOL_NAME = 'load_skill';
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Config
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface LoadSkillToolConfig {
|
|
45
|
+
/** The skill registry to load skills from. */
|
|
46
|
+
registry: SkillRegistry;
|
|
47
|
+
/** Build the visible skills summary for the tool description. */
|
|
48
|
+
getAvailableSkillsSummary?: () => string;
|
|
49
|
+
/** The skill buffer to push loaded skills into. */
|
|
50
|
+
getSkillBuffer: () => LoadedSkill[];
|
|
51
|
+
/** Push a loaded skill to the buffer (handles deduplication). */
|
|
52
|
+
pushToSkillBuffer: (skill: LoadedSkill) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Factory
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create the load_skill tool.
|
|
61
|
+
*
|
|
62
|
+
* Returns a Cortex-native tool. CortexAgent adapts it to pi-agent-core's
|
|
63
|
+
* execute signature when synchronizing the tool inventory.
|
|
64
|
+
* The tool description includes the available skills summary, which
|
|
65
|
+
* updates when skills are added or removed from the registry.
|
|
66
|
+
*/
|
|
67
|
+
export function createLoadSkillTool(config: LoadSkillToolConfig): {
|
|
68
|
+
name: string;
|
|
69
|
+
description: string;
|
|
70
|
+
parameters: typeof LoadSkillParams;
|
|
71
|
+
execute: (args: unknown) => Promise<unknown>;
|
|
72
|
+
} {
|
|
73
|
+
return {
|
|
74
|
+
name: LOAD_SKILL_TOOL_NAME,
|
|
75
|
+
|
|
76
|
+
description: `Load a skill's full instructions into your active context. Call this tool when you need detailed guidance for a specific task. The skill's instructions will be available in your context for the remainder of this loop.
|
|
77
|
+
|
|
78
|
+
${config.getAvailableSkillsSummary ? config.getAvailableSkillsSummary() : config.registry.getAvailableSkillsSummary()}`,
|
|
79
|
+
|
|
80
|
+
parameters: LoadSkillParams,
|
|
81
|
+
|
|
82
|
+
execute: async (args: unknown): Promise<unknown> => {
|
|
83
|
+
const params = args as LoadSkillParamsType;
|
|
84
|
+
|
|
85
|
+
// Check if skill exists
|
|
86
|
+
const entry = config.registry.getEntry(params.name);
|
|
87
|
+
if (!entry) {
|
|
88
|
+
return `Unknown skill: "${params.name}". Check available skills in the tool description.`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if the skill is model-invocable
|
|
92
|
+
if (!entry.modelInvocable) {
|
|
93
|
+
return `Skill "${params.name}" is not available for direct loading.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Load and preprocess the skill body
|
|
97
|
+
try {
|
|
98
|
+
const callArgs = {
|
|
99
|
+
args: params.arguments ? params.arguments.split(/\s+/) : [],
|
|
100
|
+
rawArgs: params.arguments ?? '',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const body = await config.registry.getSkillBody(params.name, callArgs);
|
|
104
|
+
|
|
105
|
+
// Push to skill buffer (deduplication handled by pushToSkillBuffer)
|
|
106
|
+
config.pushToSkillBuffer({ name: params.name, content: body });
|
|
107
|
+
|
|
108
|
+
return `Skill "${params.name}" loaded. Full instructions are now active in your context (see the skill instructions section below the conversation history). Review them before proceeding.`;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
return `Failed to load skill "${params.name}": ${message}`;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Rebuild the load_skill tool's description with the current available
|
|
119
|
+
* skills summary. Called when skills are added or removed.
|
|
120
|
+
*
|
|
121
|
+
* Returns a function that produces the updated description string.
|
|
122
|
+
*/
|
|
123
|
+
export function buildLoadSkillDescription(
|
|
124
|
+
registry: SkillRegistry,
|
|
125
|
+
availableSkillsSummary?: string,
|
|
126
|
+
): string {
|
|
127
|
+
return `Load a skill's full instructions into your active context. Call this tool when you need detailed guidance for a specific task. The skill's instructions will be available in your context for the remainder of this loop.
|
|
128
|
+
|
|
129
|
+
${availableSkillsSummary ?? registry.getAvailableSkillsSummary()}`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubAgentManager: tracks active sub-agents, enforces concurrency limits,
|
|
3
|
+
* manages lifecycle, and delivers background completion notifications.
|
|
4
|
+
*
|
|
5
|
+
* Each sub-agent is an independent CortexAgent instance tracked by task ID.
|
|
6
|
+
* The manager does not own the CortexAgent; it tracks references and
|
|
7
|
+
* coordinates lifecycle events for the consumer.
|
|
8
|
+
*
|
|
9
|
+
* References:
|
|
10
|
+
* - docs/cortex/tools/sub-agent.md
|
|
11
|
+
* - docs/cortex/plans/phase-4-sub-agents-and-skills.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { SubAgentResult, TrackedSubAgent } from './types.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface SubAgentManagerConfig {
|
|
21
|
+
/** Maximum concurrent sub-agents. Default: 4. */
|
|
22
|
+
maxConcurrent: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SubAgentLifecycleHooks {
|
|
26
|
+
onSpawned?: (taskId: string, instructions: string, background: boolean) => void;
|
|
27
|
+
onCompleted?: (taskId: string, result: string, status: string, usage: unknown) => void;
|
|
28
|
+
onFailed?: (taskId: string, error: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// SubAgentManager
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export class SubAgentManager {
|
|
36
|
+
private readonly agents = new Map<string, TrackedSubAgent>();
|
|
37
|
+
private readonly maxConcurrent: number;
|
|
38
|
+
private hooks: SubAgentLifecycleHooks = {};
|
|
39
|
+
|
|
40
|
+
constructor(config?: Partial<SubAgentManagerConfig>) {
|
|
41
|
+
this.maxConcurrent = config?.maxConcurrent ?? 4;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set lifecycle hooks. Called by CortexAgent to wire consumer event handlers.
|
|
46
|
+
*/
|
|
47
|
+
setHooks(hooks: SubAgentLifecycleHooks): void {
|
|
48
|
+
this.hooks = hooks;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if another sub-agent can be spawned within the concurrency limit.
|
|
53
|
+
*/
|
|
54
|
+
canSpawn(): boolean {
|
|
55
|
+
return this.agents.size < this.maxConcurrent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the number of currently active sub-agents.
|
|
60
|
+
*/
|
|
61
|
+
get activeCount(): number {
|
|
62
|
+
return this.agents.size;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the concurrency limit.
|
|
67
|
+
*/
|
|
68
|
+
get limit(): number {
|
|
69
|
+
return this.maxConcurrent;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register a newly spawned sub-agent.
|
|
74
|
+
* Returns false if the concurrency limit would be exceeded.
|
|
75
|
+
*/
|
|
76
|
+
track(entry: TrackedSubAgent): boolean {
|
|
77
|
+
if (this.agents.size >= this.maxConcurrent) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.agents.set(entry.taskId, entry);
|
|
82
|
+
|
|
83
|
+
// Fire lifecycle hook
|
|
84
|
+
try {
|
|
85
|
+
this.hooks.onSpawned?.(entry.taskId, entry.instructions, entry.background);
|
|
86
|
+
} catch {
|
|
87
|
+
// Swallow hook errors
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Mark a sub-agent as completed and remove it from tracking.
|
|
95
|
+
*/
|
|
96
|
+
complete(taskId: string, result: SubAgentResult): void {
|
|
97
|
+
const entry = this.agents.get(taskId);
|
|
98
|
+
if (!entry) return;
|
|
99
|
+
|
|
100
|
+
this.agents.delete(taskId);
|
|
101
|
+
|
|
102
|
+
// Resolve the completion promise
|
|
103
|
+
entry.resolve(result);
|
|
104
|
+
|
|
105
|
+
// Fire lifecycle hook (pass full result metadata including toolCalls)
|
|
106
|
+
try {
|
|
107
|
+
const usageWithToolCalls: Record<string, unknown> = { ...result.usage };
|
|
108
|
+
if (result.toolCalls) {
|
|
109
|
+
usageWithToolCalls['toolCalls'] = result.toolCalls;
|
|
110
|
+
}
|
|
111
|
+
this.hooks.onCompleted?.(
|
|
112
|
+
taskId,
|
|
113
|
+
result.output,
|
|
114
|
+
result.status,
|
|
115
|
+
usageWithToolCalls,
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
// Swallow hook errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Mark a sub-agent as failed and remove it from tracking.
|
|
124
|
+
*/
|
|
125
|
+
fail(taskId: string, error: string): void {
|
|
126
|
+
const entry = this.agents.get(taskId);
|
|
127
|
+
if (!entry) return;
|
|
128
|
+
|
|
129
|
+
this.agents.delete(taskId);
|
|
130
|
+
|
|
131
|
+
// Resolve the completion promise with a failed result
|
|
132
|
+
entry.resolve({
|
|
133
|
+
output: '',
|
|
134
|
+
status: 'failed',
|
|
135
|
+
usage: { turns: 0, cost: 0, durationMs: Date.now() - entry.spawnedAt, contextTokens: 0 },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Fire lifecycle hook
|
|
139
|
+
try {
|
|
140
|
+
this.hooks.onFailed?.(taskId, error);
|
|
141
|
+
} catch {
|
|
142
|
+
// Swallow hook errors
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get a tracked sub-agent by task ID.
|
|
148
|
+
*/
|
|
149
|
+
get(taskId: string): TrackedSubAgent | undefined {
|
|
150
|
+
return this.agents.get(taskId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Update tool activity for a running sub-agent.
|
|
155
|
+
* Called when child tool_call_start events are forwarded via EventBridge.
|
|
156
|
+
*/
|
|
157
|
+
updateToolActivity(taskId: string, toolName: string, summary: string): void {
|
|
158
|
+
const entry = this.agents.get(taskId);
|
|
159
|
+
if (!entry) return;
|
|
160
|
+
entry.toolCount++;
|
|
161
|
+
entry.lastToolName = toolName;
|
|
162
|
+
entry.lastToolSummary = summary;
|
|
163
|
+
entry.lastToolStartedAt = Date.now();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all active sub-agent task IDs.
|
|
168
|
+
*/
|
|
169
|
+
getActiveTaskIds(): string[] {
|
|
170
|
+
return [...this.agents.keys()];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get completion promises for all background sub-agents.
|
|
175
|
+
* Used to build follow-up messages when background agents complete.
|
|
176
|
+
*/
|
|
177
|
+
getBackgroundCompletions(): Array<{ taskId: string; completion: Promise<SubAgentResult> }> {
|
|
178
|
+
const results: Array<{ taskId: string; completion: Promise<SubAgentResult> }> = [];
|
|
179
|
+
for (const [taskId, entry] of this.agents) {
|
|
180
|
+
if (entry.background) {
|
|
181
|
+
results.push({ taskId, completion: entry.completion });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Cancel all active sub-agents. Called during parent destroy().
|
|
189
|
+
* Aborts each sub-agent and removes it from tracking.
|
|
190
|
+
*
|
|
191
|
+
* @param abortFn - Function to abort a CortexAgent (passed to avoid circular dep)
|
|
192
|
+
*/
|
|
193
|
+
async cancelAll(abortFn: (agent: unknown) => Promise<void>): Promise<void> {
|
|
194
|
+
const entries = [...this.agents.values()];
|
|
195
|
+
this.agents.clear();
|
|
196
|
+
|
|
197
|
+
const settled = await Promise.allSettled(
|
|
198
|
+
entries.map(async (entry) => {
|
|
199
|
+
try {
|
|
200
|
+
await abortFn(entry.agent);
|
|
201
|
+
} catch {
|
|
202
|
+
// Best-effort abort
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Resolve the completion promise as cancelled
|
|
206
|
+
entry.resolve({
|
|
207
|
+
output: '',
|
|
208
|
+
status: 'cancelled',
|
|
209
|
+
usage: { turns: 0, cost: 0, durationMs: Date.now() - entry.spawnedAt, contextTokens: 0 },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Fire failure hook
|
|
213
|
+
try {
|
|
214
|
+
this.hooks.onFailed?.(entry.taskId, 'Parent agent destroyed');
|
|
215
|
+
} catch {
|
|
216
|
+
// Swallow hook errors
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Log any unexpected errors (consumer should provide logging)
|
|
222
|
+
for (const result of settled) {
|
|
223
|
+
if (result.status === 'rejected') {
|
|
224
|
+
// Swallowed: best-effort cleanup
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up all state. Called during parent destroy().
|
|
231
|
+
*/
|
|
232
|
+
destroy(): void {
|
|
233
|
+
this.agents.clear();
|
|
234
|
+
this.hooks = {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic token estimation.
|
|
3
|
+
*
|
|
4
|
+
* Uses character-based heuristic (chars / 4), the community standard and
|
|
5
|
+
* closest to Anthropic's official recommendation (chars / 3.5).
|
|
6
|
+
* Character-based is more stable than word-based across content types
|
|
7
|
+
* (prose, code, JSON, markdown).
|
|
8
|
+
*
|
|
9
|
+
* This is a duplicate of the same utility in @animus-labs/shared,
|
|
10
|
+
* kept inline to avoid a dependency from cortex to shared.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Estimate the number of tokens in a text string.
|
|
15
|
+
*
|
|
16
|
+
* Uses chars / 4 heuristic (community standard, ~15% underestimate for Claude).
|
|
17
|
+
* Not a tokenizer; a fast estimation for budget decisions and compaction triggers.
|
|
18
|
+
* For exact counts, use the Anthropic count_tokens API.
|
|
19
|
+
*
|
|
20
|
+
* @param text - The text to estimate tokens for
|
|
21
|
+
* @returns Estimated token count (always at least 0, rounded up)
|
|
22
|
+
*/
|
|
23
|
+
export function estimateTokens(text: string): number {
|
|
24
|
+
if (!text) return 0;
|
|
25
|
+
return Math.ceil(text.length / 4);
|
|
26
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { ToolExecuteContext } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cortex's canonical in-process tool contract.
|
|
5
|
+
*
|
|
6
|
+
* All tools registered with CortexAgent are normalized to this signature.
|
|
7
|
+
* Cortex adapts this shape to pi-agent-core's execute signature at the
|
|
8
|
+
* registration boundary.
|
|
9
|
+
*/
|
|
10
|
+
export interface CortexTool<TParams = unknown, TResult = unknown> {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
parameters: unknown;
|
|
14
|
+
execute: (params: TParams, context?: ToolExecuteContext) => Promise<TResult>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Marks this tool as eligible for deferred loading. When the agent has
|
|
18
|
+
* `deferredTools.enabled = true`, deferred tools are NOT included in the
|
|
19
|
+
* `tools` array sent to the model on every turn. Instead, only their names
|
|
20
|
+
* appear in the `_available_tools` slot, and the model uses ToolSearch to
|
|
21
|
+
* load full schemas on demand.
|
|
22
|
+
*
|
|
23
|
+
* MCP tools get `isMcp: true` set automatically by the MCP client and are
|
|
24
|
+
* deferred when `deferredTools.deferMcp` is true (default). Built-in or
|
|
25
|
+
* consumer-supplied tools can opt in via `shouldDefer: true`.
|
|
26
|
+
*/
|
|
27
|
+
shouldDefer?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Forces this tool to always be sent in the `tools` array, even if it
|
|
31
|
+
* matches deferral criteria (e.g., an MCP tool the consumer wants always
|
|
32
|
+
* available). Overrides `shouldDefer` and the `deferMcp` config.
|
|
33
|
+
*/
|
|
34
|
+
alwaysLoad?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Marker indicating this tool was wrapped from an MCP server. Set
|
|
38
|
+
* automatically by the MCP client. Consumers should not set this manually.
|
|
39
|
+
*/
|
|
40
|
+
isMcp?: boolean;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optional pi-agent-core execution hint. Use "sequential" for tools that
|
|
44
|
+
* must update shared agent state before later tool calls in the same batch.
|
|
45
|
+
*/
|
|
46
|
+
executionMode?: 'sequential' | 'parallel';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Raw pi-agent-core tool contract.
|
|
51
|
+
*
|
|
52
|
+
* Use fromPiAgentTool() to explicitly adapt a tool with this signature into
|
|
53
|
+
* Cortex's canonical CortexTool shape.
|
|
54
|
+
*/
|
|
55
|
+
export interface PiAgentTool<TParams = unknown, TResult = unknown> {
|
|
56
|
+
name: string;
|
|
57
|
+
description: string;
|
|
58
|
+
parameters: unknown;
|
|
59
|
+
executionMode?: 'sequential' | 'parallel';
|
|
60
|
+
execute: (
|
|
61
|
+
toolCallId: string,
|
|
62
|
+
params: TParams,
|
|
63
|
+
signal?: AbortSignal,
|
|
64
|
+
onUpdate?: (partialResult: unknown) => void,
|
|
65
|
+
) => Promise<TResult>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Explicitly adapt a pi-agent-core-style tool into Cortex's canonical tool contract.
|
|
70
|
+
*/
|
|
71
|
+
export function fromPiAgentTool<TParams = unknown, TResult = unknown>(
|
|
72
|
+
tool: PiAgentTool<TParams, TResult>,
|
|
73
|
+
): CortexTool<TParams, TResult> {
|
|
74
|
+
const cortexTool: CortexTool<TParams, TResult> = {
|
|
75
|
+
name: tool.name,
|
|
76
|
+
description: tool.description,
|
|
77
|
+
parameters: tool.parameters,
|
|
78
|
+
execute: (params: TParams, context?: ToolExecuteContext) => {
|
|
79
|
+
return tool.execute(
|
|
80
|
+
context?.toolCallId ?? `${tool.name}-direct`,
|
|
81
|
+
params,
|
|
82
|
+
context?.signal,
|
|
83
|
+
context?.onUpdate as ((partialResult: unknown) => void) | undefined,
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (tool.executionMode) {
|
|
88
|
+
cortexTool.executionMode = tool.executionMode;
|
|
89
|
+
}
|
|
90
|
+
return cortexTool;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate that a tool matches Cortex's canonical execute signature.
|
|
95
|
+
*
|
|
96
|
+
* Cortex does not infer tool execution contracts from function arity. Tools
|
|
97
|
+
* that already use pi-agent-core's raw execute signature must be adapted
|
|
98
|
+
* explicitly with fromPiAgentTool().
|
|
99
|
+
*/
|
|
100
|
+
export function assertValidCortexTool(tool: CortexTool): CortexTool {
|
|
101
|
+
if (typeof tool.execute !== 'function') {
|
|
102
|
+
throw new Error(`Tool "${tool.name}" is missing an execute() function.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tool.execute.length > 2) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Tool "${tool.name}" does not use Cortex's execute(params, context?) contract. ` +
|
|
108
|
+
'Wrap raw pi-agent-core tools with fromPiAgentTool() before passing them to CortexAgent.create().',
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return tool;
|
|
113
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool result persistence: bookend-and-persist for oversized tool results.
|
|
3
|
+
*
|
|
4
|
+
* Sits at the tool execution boundary (refreshTools wrapper). After a tool
|
|
5
|
+
* returns its result, this module checks whether the text content exceeds
|
|
6
|
+
* the per-result threshold and either:
|
|
7
|
+
* - passes through unchanged (under threshold or skipped tool)
|
|
8
|
+
* - persists to disk (when `persistResult` is configured) and returns
|
|
9
|
+
* a bookend preview (head + tail) plus a file reference
|
|
10
|
+
* - returns a bookend preview only (when no `persistResult` is configured)
|
|
11
|
+
*
|
|
12
|
+
* This replaces ad-hoc per-tool truncation (Grep, Bash, WebFetch) with a
|
|
13
|
+
* single, uniform mechanism. Reuses `applyBookend` and `getToolCategory`
|
|
14
|
+
* from the existing compaction infrastructure.
|
|
15
|
+
*
|
|
16
|
+
* Reference: docs/cortex/tool-result-persistence.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PersistResultFn, ToolCategory } from './types.js';
|
|
20
|
+
import { applyBookend, getToolCategory } from './compaction/microcompaction.js';
|
|
21
|
+
import { estimateTokens } from './token-estimator.js';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/** Default per-tool token threshold. Results larger than this trigger persistence/bookend. */
|
|
28
|
+
export const MAX_RESULT_TOKENS = 25_000;
|
|
29
|
+
|
|
30
|
+
/** Bookend size for the preview (head and tail each). 1,500 chars ≈ 375 tokens. */
|
|
31
|
+
export const BOOKEND_CHARS = 1_500;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Tools whose results bypass the interceptor entirely.
|
|
35
|
+
*
|
|
36
|
+
* Either inherently bounded (Edit, Write, Glob) or content already on disk
|
|
37
|
+
* where the model can use offset/limit on the original file (Read).
|
|
38
|
+
*/
|
|
39
|
+
export const SKIP_RESULT_PERSISTENCE = new Set<string>([
|
|
40
|
+
'Read',
|
|
41
|
+
'Edit',
|
|
42
|
+
'UndoEdit',
|
|
43
|
+
'Write',
|
|
44
|
+
'Glob',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Built-in per-tool threshold overrides. Tools listed here use a different
|
|
49
|
+
* token limit than `MAX_RESULT_TOKENS` (25K).
|
|
50
|
+
*
|
|
51
|
+
* Rationale per tool:
|
|
52
|
+
* - Bash: command output is verbose with low signal density (logs, stack
|
|
53
|
+
* traces, build spam). A tighter cap reduces noise in context while still
|
|
54
|
+
* preserving full output via persistence.
|
|
55
|
+
*
|
|
56
|
+
* Consumers can extend or override this map via `CortexAgentConfig.toolResultThresholds`.
|
|
57
|
+
*/
|
|
58
|
+
export const DEFAULT_TOOL_THRESHOLDS: Record<string, number> = {
|
|
59
|
+
Bash: 7_500,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the effective threshold for a tool.
|
|
64
|
+
* Order of precedence: consumer overrides > built-in defaults > MAX_RESULT_TOKENS.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveThreshold(
|
|
67
|
+
toolName: string,
|
|
68
|
+
consumerOverrides?: Record<string, number>,
|
|
69
|
+
): number {
|
|
70
|
+
if (consumerOverrides && toolName in consumerOverrides) return consumerOverrides[toolName]!;
|
|
71
|
+
if (toolName in DEFAULT_TOOL_THRESHOLDS) return DEFAULT_TOOL_THRESHOLDS[toolName]!;
|
|
72
|
+
return MAX_RESULT_TOKENS;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// API
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
export interface ApplyPersistenceOptions {
|
|
80
|
+
toolName: string;
|
|
81
|
+
toolCallId: string;
|
|
82
|
+
persistResult?: PersistResultFn | undefined;
|
|
83
|
+
toolCategories?: Record<string, ToolCategory> | undefined;
|
|
84
|
+
/** Consumer-provided per-tool threshold overrides (in tokens). */
|
|
85
|
+
thresholds?: Record<string, number> | undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Options for processing a full tool result (with potentially multiple parts).
|
|
90
|
+
* Same shape as ApplyPersistenceOptions minus the per-call identifiers.
|
|
91
|
+
*/
|
|
92
|
+
export interface ProcessResultOptions {
|
|
93
|
+
toolName: string;
|
|
94
|
+
toolCallId: string;
|
|
95
|
+
persistResult?: PersistResultFn | undefined;
|
|
96
|
+
toolCategories?: Record<string, ToolCategory> | undefined;
|
|
97
|
+
thresholds?: Record<string, number> | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Process a tool result text part for size limiting.
|
|
102
|
+
*
|
|
103
|
+
* - Under threshold or skipped tool: returns content unchanged
|
|
104
|
+
* - Over threshold + `persistResult` configured: persists, returns bookend + file ref
|
|
105
|
+
* - Over threshold + no `persistResult`: returns bookend only (lossy, but bounded)
|
|
106
|
+
*
|
|
107
|
+
* Pure async helper; never throws. Persist failures fall back to bookend-only.
|
|
108
|
+
*/
|
|
109
|
+
export async function applyResultPersistence(
|
|
110
|
+
content: string,
|
|
111
|
+
options: ApplyPersistenceOptions,
|
|
112
|
+
): Promise<string> {
|
|
113
|
+
if (SKIP_RESULT_PERSISTENCE.has(options.toolName)) return content;
|
|
114
|
+
|
|
115
|
+
const threshold = resolveThreshold(options.toolName, options.thresholds);
|
|
116
|
+
const tokens = estimateTokens(content);
|
|
117
|
+
if (tokens <= threshold) return content;
|
|
118
|
+
|
|
119
|
+
const bookended = applyBookend(content, BOOKEND_CHARS, BOOKEND_CHARS, tokens);
|
|
120
|
+
|
|
121
|
+
if (options.persistResult) {
|
|
122
|
+
const category = getToolCategory(options.toolName, options.toolCategories) ?? 'ephemeral';
|
|
123
|
+
try {
|
|
124
|
+
const path = await options.persistResult(content, {
|
|
125
|
+
toolName: options.toolName,
|
|
126
|
+
toolCallId: options.toolCallId,
|
|
127
|
+
category,
|
|
128
|
+
});
|
|
129
|
+
return [
|
|
130
|
+
`[Result persisted: ${path} (${content.length.toLocaleString()} chars, ~${tokens.toLocaleString()} tokens)]`,
|
|
131
|
+
'',
|
|
132
|
+
bookended,
|
|
133
|
+
'',
|
|
134
|
+
'Use the Read tool with offset/limit to examine specific sections.',
|
|
135
|
+
].join('\n');
|
|
136
|
+
} catch {
|
|
137
|
+
// Persist failed; fall through to bookend-only path below.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
`[Result truncated: ~${tokens.toLocaleString()} tokens exceeded ${threshold.toLocaleString()} token limit]`,
|
|
143
|
+
'',
|
|
144
|
+
bookended,
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Process a full tool result (potentially multi-part) through the
|
|
150
|
+
* persistence interceptor.
|
|
151
|
+
*
|
|
152
|
+
* - Iterates the `content` array
|
|
153
|
+
* - For each `text` part, runs `applyResultPersistence`
|
|
154
|
+
* - Other part types (e.g., `image`) pass through unchanged
|
|
155
|
+
* - Returns the same object reference if nothing changed (no allocation)
|
|
156
|
+
*
|
|
157
|
+
* Used by `CortexAgent.refreshTools()` at the tool execution boundary.
|
|
158
|
+
* Exported so the wrapper logic is unit-testable.
|
|
159
|
+
*/
|
|
160
|
+
export async function processToolResult(
|
|
161
|
+
result: unknown,
|
|
162
|
+
options: ProcessResultOptions,
|
|
163
|
+
): Promise<unknown> {
|
|
164
|
+
if (!result || typeof result !== 'object') return result;
|
|
165
|
+
const asObj = result as Record<string, unknown>;
|
|
166
|
+
const content = asObj['content'];
|
|
167
|
+
if (!Array.isArray(content) || content.length === 0) return result;
|
|
168
|
+
|
|
169
|
+
let modified = false;
|
|
170
|
+
const newContent = await Promise.all(
|
|
171
|
+
content.map(async (part: unknown) => {
|
|
172
|
+
if (
|
|
173
|
+
part &&
|
|
174
|
+
typeof part === 'object' &&
|
|
175
|
+
(part as Record<string, unknown>)['type'] === 'text' &&
|
|
176
|
+
typeof (part as Record<string, unknown>)['text'] === 'string'
|
|
177
|
+
) {
|
|
178
|
+
const text = (part as { text: string }).text;
|
|
179
|
+
const processed = await applyResultPersistence(text, {
|
|
180
|
+
toolName: options.toolName,
|
|
181
|
+
toolCallId: options.toolCallId,
|
|
182
|
+
persistResult: options.persistResult,
|
|
183
|
+
toolCategories: options.toolCategories,
|
|
184
|
+
thresholds: options.thresholds,
|
|
185
|
+
});
|
|
186
|
+
if (processed !== text) {
|
|
187
|
+
modified = true;
|
|
188
|
+
return { ...(part as Record<string, unknown>), text: processed };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return part;
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!modified) return result;
|
|
196
|
+
return { ...asObj, content: newContent };
|
|
197
|
+
}
|