clementine-agent 1.18.41 → 1.18.43
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.
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — AgentDefinition factory.
|
|
3
|
+
*
|
|
4
|
+
* The canonical Claude Agent SDK pattern is to pass `agents: { ... }`
|
|
5
|
+
* to `query()`, where each entry is an `AgentDefinition`. Claude routes
|
|
6
|
+
* subwork to subagents based on each definition's `description` field.
|
|
7
|
+
*
|
|
8
|
+
* Today's Clementine has multiple parallel orchestration paths
|
|
9
|
+
* (PlanOrchestrator, runUnleashedTask phases, fanout-policy directive,
|
|
10
|
+
* pre-LLM plan routing). This file is the start of consolidating all
|
|
11
|
+
* of that into the SDK-native subagent pattern.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const agents = buildAgentMap({ profileManager, isAutonomous: false });
|
|
15
|
+
* query({ prompt, options: { agents, ... } })
|
|
16
|
+
*
|
|
17
|
+
* Phase 1 (1.18.43): this file is created but not wired into production
|
|
18
|
+
* yet. The dashboard's /api/runagent/test endpoint exercises it for
|
|
19
|
+
* verification before any real migration.
|
|
20
|
+
*/
|
|
21
|
+
import type { AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
|
|
22
|
+
import type { AgentManager } from './agent-manager.js';
|
|
23
|
+
export interface BuildAgentMapOptions {
|
|
24
|
+
/** Source of hired-agent profiles. When undefined, only the system subagents are returned. */
|
|
25
|
+
profileManager?: AgentManager;
|
|
26
|
+
/** When true, restrict the surface to safe-for-cron subagents (no chat-only ones). */
|
|
27
|
+
isAutonomous?: boolean;
|
|
28
|
+
/** Active agent slug — when set, hired agents OTHER than this one still get definitions
|
|
29
|
+
* but the active one's profile-as-system-prompt is handled by the caller. */
|
|
30
|
+
activeAgentSlug?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the AgentDefinition map for a runAgent call. Mix of system
|
|
34
|
+
* subagents (planner, researcher, cron-fixer) and hired-agent profiles.
|
|
35
|
+
*
|
|
36
|
+
* The system subagents are intentionally minimal — they exist so Claude
|
|
37
|
+
* can route specific kinds of work cleanly. Add new ones (per the
|
|
38
|
+
* migration plan) as we collapse other orchestration paths.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildAgentMap(opts?: BuildAgentMapOptions): Record<string, AgentDefinition>;
|
|
41
|
+
/** Type guard helper for callers. */
|
|
42
|
+
export declare function hasAgent(map: Record<string, AgentDefinition>, slug: string): boolean;
|
|
43
|
+
//# sourceMappingURL=agent-definitions.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — AgentDefinition factory.
|
|
3
|
+
*
|
|
4
|
+
* The canonical Claude Agent SDK pattern is to pass `agents: { ... }`
|
|
5
|
+
* to `query()`, where each entry is an `AgentDefinition`. Claude routes
|
|
6
|
+
* subwork to subagents based on each definition's `description` field.
|
|
7
|
+
*
|
|
8
|
+
* Today's Clementine has multiple parallel orchestration paths
|
|
9
|
+
* (PlanOrchestrator, runUnleashedTask phases, fanout-policy directive,
|
|
10
|
+
* pre-LLM plan routing). This file is the start of consolidating all
|
|
11
|
+
* of that into the SDK-native subagent pattern.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const agents = buildAgentMap({ profileManager, isAutonomous: false });
|
|
15
|
+
* query({ prompt, options: { agents, ... } })
|
|
16
|
+
*
|
|
17
|
+
* Phase 1 (1.18.43): this file is created but not wired into production
|
|
18
|
+
* yet. The dashboard's /api/runagent/test endpoint exercises it for
|
|
19
|
+
* verification before any real migration.
|
|
20
|
+
*/
|
|
21
|
+
const PLANNER_PROMPT = [
|
|
22
|
+
'You are a task planner for Clementine. You receive a multi-step request from the parent agent.',
|
|
23
|
+
'',
|
|
24
|
+
'Your job: decompose the request into ATOMIC, parallel-safe steps, then return a JSON plan.',
|
|
25
|
+
'',
|
|
26
|
+
'Output ONLY a JSON object (no markdown fences, no prose):',
|
|
27
|
+
'{',
|
|
28
|
+
' "steps": [',
|
|
29
|
+
' { "id": "step-1", "description": "...", "subagent": "researcher|cron-fixer|...|null", "prompt": "...", "model": "haiku|sonnet", "dependsOn": [] }',
|
|
30
|
+
' ],',
|
|
31
|
+
' "synthesisHint": "How the parent should combine step outputs"',
|
|
32
|
+
'}',
|
|
33
|
+
'',
|
|
34
|
+
'Rules:',
|
|
35
|
+
'- 2-8 steps. Atomic = completes in 5-30 tool calls.',
|
|
36
|
+
'- MAXIMIZE parallelism: independent steps have empty dependsOn.',
|
|
37
|
+
'- Pick the right subagent per step:',
|
|
38
|
+
' - `researcher` for per-item lookups (1 lead, 1 account, 1 file): model=haiku',
|
|
39
|
+
' - `cron-fixer` for diagnose-and-apply on broken cron jobs: model=sonnet',
|
|
40
|
+
' - null (parent runs the step) for synthesis or when no specialist fits',
|
|
41
|
+
'- Each step prompt is SELF-CONTAINED — the sub agent sees no parent history.',
|
|
42
|
+
'- End each step prompt with "Deliver: <one-line return shape>".',
|
|
43
|
+
].join('\n');
|
|
44
|
+
const RESEARCHER_PROMPT = [
|
|
45
|
+
'You are a per-item research specialist. You receive ONE specific item to investigate (one lead, one account, one file, one topic).',
|
|
46
|
+
'',
|
|
47
|
+
'Use your bounded tools to gather the requested information. Return a ONE-PARAGRAPH summary in the format the parent specified.',
|
|
48
|
+
'',
|
|
49
|
+
'NEVER return raw tool output, full lists, or unbounded data. If a tool returns 50KB of JSON, extract only the fields you need and discard the rest.',
|
|
50
|
+
'',
|
|
51
|
+
'If you cannot find the requested data, say so in one line. Do not speculate.',
|
|
52
|
+
].join('\n');
|
|
53
|
+
const CRON_FIXER_PROMPT = [
|
|
54
|
+
'You are the cron-fix specialist. You diagnose and apply fixes to broken cron jobs.',
|
|
55
|
+
'',
|
|
56
|
+
'Workflow:',
|
|
57
|
+
'1. Call `list_broken_jobs` to see what is currently broken with their cached diagnoses.',
|
|
58
|
+
'2. For each job the user/parent asked about, check the proposed fix:',
|
|
59
|
+
' - confidence=high + risk=low + autoApply=true → call `apply_broken_job_fix`.',
|
|
60
|
+
' - Otherwise → describe the diagnosis and ask the parent for explicit approval.',
|
|
61
|
+
'3. After applying a fix, the verification system auto-rolls-back if the next 3 runs do not improve. You do NOT need to monitor manually.',
|
|
62
|
+
'',
|
|
63
|
+
'Return: a one-paragraph summary of what you applied (or what is blocking apply), per job.',
|
|
64
|
+
].join('\n');
|
|
65
|
+
/** Map a hired-agent profile to an AgentDefinition.
|
|
66
|
+
* Used when Clementine wants to delegate to Ross/Sasha/Nora etc. */
|
|
67
|
+
function profileToAgentDefinition(p) {
|
|
68
|
+
return {
|
|
69
|
+
description: p.description ?? `${p.name} (hired agent: ${p.slug})`,
|
|
70
|
+
prompt: p.systemPromptBody ?? `You are ${p.name}.`,
|
|
71
|
+
// Honor explicit allowlist when present; otherwise inherit from parent.
|
|
72
|
+
...(p.team?.allowedTools?.length ? { tools: p.team.allowedTools } : {}),
|
|
73
|
+
// Hired agents keep their configured model (Sonnet by default).
|
|
74
|
+
...(p.model ? { model: p.model } : { model: 'sonnet' }),
|
|
75
|
+
// Effort: hired agents do real work, default medium. Caller can override.
|
|
76
|
+
effort: 'medium',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build the AgentDefinition map for a runAgent call. Mix of system
|
|
81
|
+
* subagents (planner, researcher, cron-fixer) and hired-agent profiles.
|
|
82
|
+
*
|
|
83
|
+
* The system subagents are intentionally minimal — they exist so Claude
|
|
84
|
+
* can route specific kinds of work cleanly. Add new ones (per the
|
|
85
|
+
* migration plan) as we collapse other orchestration paths.
|
|
86
|
+
*/
|
|
87
|
+
export function buildAgentMap(opts = {}) {
|
|
88
|
+
const map = {};
|
|
89
|
+
// ── System subagents ────────────────────────────────────────────
|
|
90
|
+
// Planner: opus, no tools, single turn. Used when the parent agent
|
|
91
|
+
// sees a multi-step request and wants a decomposition.
|
|
92
|
+
map['planner'] = {
|
|
93
|
+
description: 'Decompose a multi-step user request into atomic, parallel-safe steps. Use for "research these N items", "build a comprehensive X", "for each Y do Z", or any request that obviously involves multiple distinct sub-tasks. Returns a JSON plan; the parent then executes the steps (often by spawning more subagents per step).',
|
|
94
|
+
prompt: PLANNER_PROMPT,
|
|
95
|
+
model: 'opus',
|
|
96
|
+
tools: [], // pure reasoning, no tools
|
|
97
|
+
effort: 'high',
|
|
98
|
+
maxTurns: 1,
|
|
99
|
+
};
|
|
100
|
+
// Researcher: haiku, per-item investigation. Cheap fan-out target.
|
|
101
|
+
map['researcher'] = {
|
|
102
|
+
description: 'Investigate ONE specific item (one lead, one account, one file, one topic) and return a one-paragraph summary. Use for per-item parallel work spawned by the planner. Cheap and fast.',
|
|
103
|
+
prompt: RESEARCHER_PROMPT,
|
|
104
|
+
model: 'haiku',
|
|
105
|
+
tools: ['Read', 'Grep', 'Glob', 'Bash', 'WebSearch', 'WebFetch'],
|
|
106
|
+
effort: 'low',
|
|
107
|
+
maxTurns: 15,
|
|
108
|
+
};
|
|
109
|
+
// Cron-fixer: sonnet, owns the broken-job diagnose+apply path.
|
|
110
|
+
// Tools restricted to the canonical fix path (no parallel mechanisms).
|
|
111
|
+
map['cron-fixer'] = {
|
|
112
|
+
description: 'Diagnose and apply fixes to broken cron jobs. Use when the user says "fix X" referring to a job, asks "what jobs are failing", or asks to re-run/repair a cron. Owns the canonical diagnosis-to-apply flow.',
|
|
113
|
+
prompt: CRON_FIXER_PROMPT,
|
|
114
|
+
model: 'sonnet',
|
|
115
|
+
tools: [
|
|
116
|
+
'mcp__clementine-tools__list_broken_jobs',
|
|
117
|
+
'mcp__clementine-tools__apply_broken_job_fix',
|
|
118
|
+
'mcp__clementine-tools__cron_list',
|
|
119
|
+
'mcp__clementine-tools__cron_run_history',
|
|
120
|
+
'Read',
|
|
121
|
+
'Grep',
|
|
122
|
+
],
|
|
123
|
+
effort: 'medium',
|
|
124
|
+
maxTurns: 10,
|
|
125
|
+
};
|
|
126
|
+
// ── Hired-agent profiles ────────────────────────────────────────
|
|
127
|
+
// Each becomes a subagent the main agent can delegate to.
|
|
128
|
+
// The "main" agent for a DM-to-bot session is set by the caller
|
|
129
|
+
// (still uses the profile's identity); these definitions cover the
|
|
130
|
+
// case where Clementine wants to invoke them mid-conversation.
|
|
131
|
+
if (opts.profileManager) {
|
|
132
|
+
const profiles = opts.profileManager.listAll();
|
|
133
|
+
for (const profile of profiles) {
|
|
134
|
+
// Skip clementine herself (she's the main agent, not a subagent)
|
|
135
|
+
if (profile.slug === 'clementine')
|
|
136
|
+
continue;
|
|
137
|
+
// Skip the active agent (don't make them their own subagent)
|
|
138
|
+
if (opts.activeAgentSlug && profile.slug === opts.activeAgentSlug)
|
|
139
|
+
continue;
|
|
140
|
+
map[profile.slug] = profileToAgentDefinition(profile);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return map;
|
|
144
|
+
}
|
|
145
|
+
/** Type guard helper for callers. */
|
|
146
|
+
export function hasAgent(map, slug) {
|
|
147
|
+
return Object.prototype.hasOwnProperty.call(map, slug);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=agent-definitions.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — runAgent: canonical Claude Agent SDK wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* This is the new code path that will eventually replace runCronJob /
|
|
8
|
+
* runUnleashedTask / runHeartbeat / runTeamTask / chat. For now it
|
|
9
|
+
* runs in PARALLEL with those — only the dashboard's
|
|
10
|
+
* /api/runagent/test endpoint exercises it. Production traffic still
|
|
11
|
+
* uses legacy paths until Phase 2.
|
|
12
|
+
*
|
|
13
|
+
* Design principles (from the SDK docs):
|
|
14
|
+
* 1. ONE query() call — no nested phase wrappers.
|
|
15
|
+
* 2. Subagents via the `agents` param — not via prompt-injected
|
|
16
|
+
* fanout directives.
|
|
17
|
+
* 3. SDK handles: agent loop, compaction, tool execution, parallel
|
|
18
|
+
* sub-spawning, prompt caching, session resume.
|
|
19
|
+
* 4. App handles: prompt + options assembly, transcript mirroring,
|
|
20
|
+
* cost logging, channel delivery.
|
|
21
|
+
* 5. NO context-thrash recovery, NO manual session rotation, NO
|
|
22
|
+
* long-task preflight, NO mode=unleashed wrapper.
|
|
23
|
+
*/
|
|
24
|
+
import { type AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
import type { AgentProfile } from '../types.js';
|
|
26
|
+
import type { AgentManager } from './agent-manager.js';
|
|
27
|
+
import type { MemoryStore } from '../memory/store.js';
|
|
28
|
+
export interface RunAgentOptions {
|
|
29
|
+
/** Stable session key for this conversation/run. Used for transcript mirroring + resume. */
|
|
30
|
+
sessionKey: string;
|
|
31
|
+
/** Source classification for telemetry: 'chat' | 'cron' | 'heartbeat' | 'team-task' | 'test'. */
|
|
32
|
+
source: string;
|
|
33
|
+
/** Optional hired-agent profile. When set, this profile becomes the MAIN
|
|
34
|
+
* agent (its system prompt is appended). When unset, Clementine is the main agent. */
|
|
35
|
+
profile?: AgentProfile | null;
|
|
36
|
+
/** Optional subagent slug to invoke explicitly (bypasses Claude's automatic routing).
|
|
37
|
+
* When set, the prompt is wrapped to direct Claude to use this subagent first. */
|
|
38
|
+
forceSubagent?: string | null;
|
|
39
|
+
/** Hired-agent registry — used to construct the AgentDefinition map for delegation. */
|
|
40
|
+
agentManager?: AgentManager | null;
|
|
41
|
+
/** Memory store for transcript mirroring + cost logging. */
|
|
42
|
+
memoryStore?: MemoryStore | null;
|
|
43
|
+
/** Optional model override. Defaults to SDK default (Sonnet) unless profile sets one. */
|
|
44
|
+
model?: string;
|
|
45
|
+
/** Reasoning effort. Defaults vary by source: chat='medium', cron='medium', heartbeat='low'. */
|
|
46
|
+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
47
|
+
/** Hard budget cap (USD). Default varies by source. SDK aborts the run when hit. */
|
|
48
|
+
maxBudgetUsd?: number;
|
|
49
|
+
/** Hard turn cap. Default: no cap (SDK runs until done). */
|
|
50
|
+
maxTurns?: number;
|
|
51
|
+
/** Optional resume — when set, the SDK continues from the prior session. */
|
|
52
|
+
resumeSessionId?: string;
|
|
53
|
+
/** Streaming callback for partial assistant text. Best-effort. */
|
|
54
|
+
onText?: (chunk: string) => void | Promise<void>;
|
|
55
|
+
/** Streaming callback when a tool is invoked (name + input). Best-effort. */
|
|
56
|
+
onToolActivity?: (info: {
|
|
57
|
+
tool: string;
|
|
58
|
+
input: Record<string, unknown>;
|
|
59
|
+
}) => void | Promise<void>;
|
|
60
|
+
/** Abort signal — when triggered, the SDK stream is cancelled. */
|
|
61
|
+
abortSignal?: AbortSignal;
|
|
62
|
+
/** Optional override of the AgentDefinition map. Mostly for tests. */
|
|
63
|
+
agents?: Record<string, AgentDefinition>;
|
|
64
|
+
/** Optional explicit allowedTools list. When unset, falls back to a sensible default
|
|
65
|
+
* including Agent (so subagents can be spawned) + core SDK tools + Clementine MCP. */
|
|
66
|
+
allowedTools?: string[];
|
|
67
|
+
/** Optional CLAUDE.md / project setting source. Defaults to ['project']. */
|
|
68
|
+
settingSources?: ('project' | 'user' | 'local')[];
|
|
69
|
+
}
|
|
70
|
+
export interface RunAgentResult {
|
|
71
|
+
/** Final text response from the agent. */
|
|
72
|
+
text: string;
|
|
73
|
+
/** Total cost in USD as reported by the SDK. */
|
|
74
|
+
totalCostUsd: number;
|
|
75
|
+
/** Number of agentic turns the loop took. */
|
|
76
|
+
numTurns: number;
|
|
77
|
+
/** SDK session ID — capture for resume. */
|
|
78
|
+
sessionId: string;
|
|
79
|
+
/** Final stop reason from the SDK (success, error_max_turns, error_max_budget_usd, etc). */
|
|
80
|
+
subtype: string;
|
|
81
|
+
/** Token usage breakdown (input, output, cache). */
|
|
82
|
+
usage?: {
|
|
83
|
+
input_tokens?: number;
|
|
84
|
+
output_tokens?: number;
|
|
85
|
+
cache_read_input_tokens?: number;
|
|
86
|
+
cache_creation_input_tokens?: number;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Run a single agent invocation via the canonical SDK pattern.
|
|
91
|
+
*
|
|
92
|
+
* Returns when the SDK loop completes (final assistant message with no
|
|
93
|
+
* tool calls, OR maxTurns/maxBudget hit, OR error).
|
|
94
|
+
*/
|
|
95
|
+
export declare function runAgent(prompt: string, opts: RunAgentOptions): Promise<RunAgentResult>;
|
|
96
|
+
//# sourceMappingURL=run-agent.d.ts.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clementine TypeScript — runAgent: canonical Claude Agent SDK wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 of the SDK-canonical migration (see
|
|
5
|
+
* /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md).
|
|
6
|
+
*
|
|
7
|
+
* This is the new code path that will eventually replace runCronJob /
|
|
8
|
+
* runUnleashedTask / runHeartbeat / runTeamTask / chat. For now it
|
|
9
|
+
* runs in PARALLEL with those — only the dashboard's
|
|
10
|
+
* /api/runagent/test endpoint exercises it. Production traffic still
|
|
11
|
+
* uses legacy paths until Phase 2.
|
|
12
|
+
*
|
|
13
|
+
* Design principles (from the SDK docs):
|
|
14
|
+
* 1. ONE query() call — no nested phase wrappers.
|
|
15
|
+
* 2. Subagents via the `agents` param — not via prompt-injected
|
|
16
|
+
* fanout directives.
|
|
17
|
+
* 3. SDK handles: agent loop, compaction, tool execution, parallel
|
|
18
|
+
* sub-spawning, prompt caching, session resume.
|
|
19
|
+
* 4. App handles: prompt + options assembly, transcript mirroring,
|
|
20
|
+
* cost logging, channel delivery.
|
|
21
|
+
* 5. NO context-thrash recovery, NO manual session rotation, NO
|
|
22
|
+
* long-task preflight, NO mode=unleashed wrapper.
|
|
23
|
+
*/
|
|
24
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
import pino from 'pino';
|
|
26
|
+
import { BASE_DIR, normalizeClaudeSdkOptionsForOneMillionContext } from '../config.js';
|
|
27
|
+
import { buildAgentMap } from './agent-definitions.js';
|
|
28
|
+
const logger = pino({ name: 'clementine.run-agent' });
|
|
29
|
+
const DEFAULT_BUDGETS = {
|
|
30
|
+
chat: 0.50,
|
|
31
|
+
cron: 1.00,
|
|
32
|
+
heartbeat: 0.25,
|
|
33
|
+
'team-task': 1.00,
|
|
34
|
+
test: 2.00,
|
|
35
|
+
};
|
|
36
|
+
const DEFAULT_EFFORTS = {
|
|
37
|
+
chat: 'medium',
|
|
38
|
+
cron: 'medium',
|
|
39
|
+
heartbeat: 'low',
|
|
40
|
+
'team-task': 'medium',
|
|
41
|
+
test: 'medium',
|
|
42
|
+
};
|
|
43
|
+
const CORE_TOOLS_FOR_AGENT_PARENT = [
|
|
44
|
+
'Agent', // REQUIRED — without this, subagents can't be invoked
|
|
45
|
+
'Read',
|
|
46
|
+
'Write',
|
|
47
|
+
'Edit',
|
|
48
|
+
'Glob',
|
|
49
|
+
'Grep',
|
|
50
|
+
'Bash',
|
|
51
|
+
'WebSearch',
|
|
52
|
+
'WebFetch',
|
|
53
|
+
'TodoWrite',
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Run a single agent invocation via the canonical SDK pattern.
|
|
57
|
+
*
|
|
58
|
+
* Returns when the SDK loop completes (final assistant message with no
|
|
59
|
+
* tool calls, OR maxTurns/maxBudget hit, OR error).
|
|
60
|
+
*/
|
|
61
|
+
export async function runAgent(prompt, opts) {
|
|
62
|
+
const source = opts.source ?? 'chat';
|
|
63
|
+
const effort = opts.effort ?? DEFAULT_EFFORTS[source] ?? 'medium';
|
|
64
|
+
const maxBudgetUsd = opts.maxBudgetUsd ?? DEFAULT_BUDGETS[source] ?? 0.50;
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
// Build the AgentDefinition map. Caller can override; otherwise we
|
|
67
|
+
// use the standard system subagents + hired-agent profiles.
|
|
68
|
+
const agents = opts.agents ?? buildAgentMap({
|
|
69
|
+
profileManager: opts.agentManager ?? undefined,
|
|
70
|
+
isAutonomous: source === 'cron' || source === 'heartbeat',
|
|
71
|
+
activeAgentSlug: opts.profile?.slug,
|
|
72
|
+
});
|
|
73
|
+
// Wrap prompt to direct Claude to a specific subagent when caller asks.
|
|
74
|
+
// Per SDK docs: explicit invocation = "Use the X agent to..."
|
|
75
|
+
const effectivePrompt = opts.forceSubagent && agents[opts.forceSubagent]
|
|
76
|
+
? `Use the ${opts.forceSubagent} agent to handle this request:\n\n${prompt}`
|
|
77
|
+
: prompt;
|
|
78
|
+
// Compose system prompt. When a hired-agent profile is active, that
|
|
79
|
+
// becomes the main agent's identity — append to the claude_code preset.
|
|
80
|
+
const profileAppend = opts.profile?.systemPromptBody
|
|
81
|
+
? opts.profile.systemPromptBody
|
|
82
|
+
: undefined;
|
|
83
|
+
// Allowed tools. Default to core + Clementine MCP. Per-subagent tool
|
|
84
|
+
// restrictions live on each AgentDefinition.tools field.
|
|
85
|
+
const allowedTools = opts.allowedTools ?? CORE_TOOLS_FOR_AGENT_PARENT;
|
|
86
|
+
// Apply 1M-context env normalization (existing infra)
|
|
87
|
+
const sdkOptionsRaw = {
|
|
88
|
+
systemPrompt: profileAppend
|
|
89
|
+
? { type: 'preset', preset: 'claude_code', append: profileAppend }
|
|
90
|
+
: { type: 'preset', preset: 'claude_code' },
|
|
91
|
+
settingSources: opts.settingSources ?? ['project'],
|
|
92
|
+
agents,
|
|
93
|
+
allowedTools,
|
|
94
|
+
permissionMode: 'bypassPermissions',
|
|
95
|
+
cwd: BASE_DIR,
|
|
96
|
+
maxBudgetUsd,
|
|
97
|
+
effort,
|
|
98
|
+
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
|
|
99
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
100
|
+
...(opts.resumeSessionId ? { resume: opts.resumeSessionId } : {}),
|
|
101
|
+
...(opts.abortSignal ? { abortController: { signal: opts.abortSignal } } : {}),
|
|
102
|
+
};
|
|
103
|
+
const sdkOptions = normalizeClaudeSdkOptionsForOneMillionContext(sdkOptionsRaw);
|
|
104
|
+
logger.info({
|
|
105
|
+
sessionKey: opts.sessionKey,
|
|
106
|
+
source,
|
|
107
|
+
profile: opts.profile?.slug,
|
|
108
|
+
forceSubagent: opts.forceSubagent,
|
|
109
|
+
effort,
|
|
110
|
+
maxBudgetUsd,
|
|
111
|
+
agentCount: Object.keys(agents).length,
|
|
112
|
+
allowedToolCount: allowedTools.length,
|
|
113
|
+
}, 'runAgent: starting query');
|
|
114
|
+
let finalText = '';
|
|
115
|
+
let sessionId = '';
|
|
116
|
+
let totalCostUsd = 0;
|
|
117
|
+
let numTurns = 0;
|
|
118
|
+
let subtype = 'unknown';
|
|
119
|
+
let usage;
|
|
120
|
+
const stream = query({ prompt: effectivePrompt, options: sdkOptions });
|
|
121
|
+
for await (const message of stream) {
|
|
122
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
123
|
+
sessionId = message.session_id ?? '';
|
|
124
|
+
logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (message.type === 'assistant') {
|
|
128
|
+
const am = message;
|
|
129
|
+
const blocks = (am.message?.content ?? []);
|
|
130
|
+
for (const block of blocks) {
|
|
131
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
132
|
+
finalText += block.text;
|
|
133
|
+
if (opts.onText) {
|
|
134
|
+
try {
|
|
135
|
+
await opts.onText(block.text);
|
|
136
|
+
}
|
|
137
|
+
catch { /* streaming is best-effort */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
141
|
+
if (opts.onToolActivity) {
|
|
142
|
+
try {
|
|
143
|
+
await opts.onToolActivity({ tool: block.name, input: block.input ?? {} });
|
|
144
|
+
}
|
|
145
|
+
catch { /* best-effort */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (message.type === 'result') {
|
|
152
|
+
const result = message;
|
|
153
|
+
sessionId = sessionId || (result.session_id ?? '');
|
|
154
|
+
subtype = result.subtype ?? 'unknown';
|
|
155
|
+
numTurns = result.num_turns ?? numTurns;
|
|
156
|
+
totalCostUsd = result.total_cost_usd ?? 0;
|
|
157
|
+
const u = result.usage;
|
|
158
|
+
if (u)
|
|
159
|
+
usage = u;
|
|
160
|
+
if (subtype === 'success') {
|
|
161
|
+
// success carries `result` field with the final text.
|
|
162
|
+
const r = result.result;
|
|
163
|
+
if (r)
|
|
164
|
+
finalText = r;
|
|
165
|
+
}
|
|
166
|
+
// Mirror cost to usage_log. Same shape as the existing
|
|
167
|
+
// logQueryResult, but standalone so we don't depend on
|
|
168
|
+
// PersonalAssistant's instance state.
|
|
169
|
+
const modelUsage = result.modelUsage;
|
|
170
|
+
if (opts.memoryStore && modelUsage) {
|
|
171
|
+
try {
|
|
172
|
+
opts.memoryStore.logUsage({
|
|
173
|
+
sessionKey: `${source}:${opts.sessionKey}`,
|
|
174
|
+
source: `runagent.${source}`,
|
|
175
|
+
modelUsage,
|
|
176
|
+
numTurns,
|
|
177
|
+
durationMs: Date.now() - startedAt,
|
|
178
|
+
agentSlug: opts.profile?.slug,
|
|
179
|
+
totalCostUsd: totalCostUsd,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger.debug({ err }, 'runAgent: usage logging failed (non-fatal)');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Other message types (UserMessage with tool_result, StreamEvent,
|
|
189
|
+
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
190
|
+
// handles compaction internally; we just let it run.
|
|
191
|
+
}
|
|
192
|
+
logger.info({
|
|
193
|
+
sessionKey: opts.sessionKey,
|
|
194
|
+
source,
|
|
195
|
+
sdkSessionId: sessionId,
|
|
196
|
+
subtype,
|
|
197
|
+
numTurns,
|
|
198
|
+
totalCostUsd: Number(totalCostUsd.toFixed(4)),
|
|
199
|
+
durationMs: Date.now() - startedAt,
|
|
200
|
+
finalTextChars: finalText.length,
|
|
201
|
+
}, 'runAgent: query complete');
|
|
202
|
+
return {
|
|
203
|
+
text: finalText,
|
|
204
|
+
totalCostUsd,
|
|
205
|
+
numTurns,
|
|
206
|
+
sessionId,
|
|
207
|
+
subtype,
|
|
208
|
+
...(usage ? { usage } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=run-agent.js.map
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -5428,6 +5428,59 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
5428
5428
|
res.status(500).json({ error: String(err) });
|
|
5429
5429
|
}
|
|
5430
5430
|
});
|
|
5431
|
+
// ── runAgent test endpoint (Phase 1 of SDK-canonical migration) ──────
|
|
5432
|
+
//
|
|
5433
|
+
// POST /api/runagent/test
|
|
5434
|
+
// body: { prompt, agentSlug?, forceSubagent?, model?, effort?, maxBudgetUsd?, source? }
|
|
5435
|
+
//
|
|
5436
|
+
// Lightweight endpoint to verify the new canonical SDK call path
|
|
5437
|
+
// without rerouting any production traffic. Owner-only.
|
|
5438
|
+
// Migration plan: /Users/nathan.reynolds/.claude/plans/sdk-canonical-migration.md
|
|
5439
|
+
app.post('/api/runagent/test', async (req, res) => {
|
|
5440
|
+
const { prompt, agentSlug, forceSubagent, model, effort, maxBudgetUsd, source } = req.body ?? {};
|
|
5441
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
5442
|
+
res.status(400).json({ error: 'prompt is required' });
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
try {
|
|
5446
|
+
const gw = await getGateway();
|
|
5447
|
+
const agentMgr = gw.getAgentManager();
|
|
5448
|
+
const profile = agentSlug ? agentMgr.get(agentSlug) ?? null : null;
|
|
5449
|
+
const memoryStore = gw.assistant.getMemoryStore?.();
|
|
5450
|
+
const { runAgent } = await import('../agent/run-agent.js');
|
|
5451
|
+
const startedAt = Date.now();
|
|
5452
|
+
const toolActivity = [];
|
|
5453
|
+
const result = await runAgent(prompt, {
|
|
5454
|
+
sessionKey: `dashboard:runagent-test:${Date.now()}`,
|
|
5455
|
+
source: typeof source === 'string' ? source : 'test',
|
|
5456
|
+
profile,
|
|
5457
|
+
forceSubagent: typeof forceSubagent === 'string' ? forceSubagent : null,
|
|
5458
|
+
agentManager: agentMgr,
|
|
5459
|
+
memoryStore: memoryStore,
|
|
5460
|
+
model: typeof model === 'string' ? model : undefined,
|
|
5461
|
+
effort: typeof effort === 'string' ? effort : undefined,
|
|
5462
|
+
maxBudgetUsd: typeof maxBudgetUsd === 'number' ? maxBudgetUsd : undefined,
|
|
5463
|
+
onToolActivity: ({ tool, input }) => {
|
|
5464
|
+
toolActivity.push({ tool, inputPreview: JSON.stringify(input).slice(0, 200) });
|
|
5465
|
+
},
|
|
5466
|
+
});
|
|
5467
|
+
res.json({
|
|
5468
|
+
ok: true,
|
|
5469
|
+
text: result.text,
|
|
5470
|
+
sessionId: result.sessionId,
|
|
5471
|
+
subtype: result.subtype,
|
|
5472
|
+
numTurns: result.numTurns,
|
|
5473
|
+
totalCostUsd: Number(result.totalCostUsd.toFixed(4)),
|
|
5474
|
+
durationMs: Date.now() - startedAt,
|
|
5475
|
+
toolCallCount: toolActivity.length,
|
|
5476
|
+
toolActivity: toolActivity.slice(0, 50), // cap for sanity
|
|
5477
|
+
usage: result.usage,
|
|
5478
|
+
});
|
|
5479
|
+
}
|
|
5480
|
+
catch (err) {
|
|
5481
|
+
res.status(500).json({ error: String(err) });
|
|
5482
|
+
}
|
|
5483
|
+
});
|
|
5431
5484
|
/** Dismiss a diagnosis without applying — clears the cached result. */
|
|
5432
5485
|
app.post('/api/cron/broken-jobs/:jobName/dismiss-diagnosis', async (req, res) => {
|
|
5433
5486
|
try {
|
|
@@ -1881,5 +1881,69 @@ export function registerAdminTools(server) {
|
|
|
1881
1881
|
const result = await runConnectNonInteractive({ allowQuitChrome: !!force_quit });
|
|
1882
1882
|
return textResult(result.message);
|
|
1883
1883
|
});
|
|
1884
|
+
// ── Broken-job diagnosis + fix-application (chat-equivalent of dashboard buttons) ──
|
|
1885
|
+
//
|
|
1886
|
+
// Before this, when the user asked "fix audit-inbox-check" in chat,
|
|
1887
|
+
// Clementine could read run logs and describe the failure but had no
|
|
1888
|
+
// tool to actually APPLY the stored fix — so she'd just keep returning
|
|
1889
|
+
// the same diagnosis text on every retry. The dashboard had the
|
|
1890
|
+
// "Apply Fix" button; the agent had nothing equivalent. These two
|
|
1891
|
+
// tools close that gap.
|
|
1892
|
+
server.tool('list_broken_jobs', 'List cron jobs that are currently failing repeatedly, with their cached diagnosis (if any) and whether each has an auto-applicable fix proposal. Use this when the user asks "what\'s broken?" or "what jobs are failing?" — it surfaces the same data the dashboard\'s broken-jobs panel shows.', {}, async () => {
|
|
1893
|
+
const { computeBrokenJobs } = await import('../gateway/failure-monitor.js');
|
|
1894
|
+
const { getDiagnosisIfFresh } = await import('../gateway/failure-diagnostics.js');
|
|
1895
|
+
const broken = computeBrokenJobs();
|
|
1896
|
+
if (broken.length === 0) {
|
|
1897
|
+
return textResult('No cron jobs are currently flagged as broken.');
|
|
1898
|
+
}
|
|
1899
|
+
const lines = [`${broken.length} cron job${broken.length === 1 ? '' : 's'} flagged as broken:`];
|
|
1900
|
+
for (const b of broken) {
|
|
1901
|
+
const d = getDiagnosisIfFresh(b.jobName);
|
|
1902
|
+
const fix = d?.proposedFix;
|
|
1903
|
+
const autoApplyAvailable = !!fix?.autoApply && d?.riskLevel === 'low';
|
|
1904
|
+
lines.push(`\n• \`${b.jobName}\``, ` failures last 48h: ${b.errorCount48h}/${b.totalRuns48h}`, b.lastErrors[0] ? ` last error: ${String(b.lastErrors[0]).slice(0, 200)}` : '', d ? ` diagnosis: ${d.rootCause?.slice(0, 200) ?? "(no root cause)"}` : ' diagnosis: pending — wait for next failure-monitor sweep', d ? ` proposed fix: type=${fix?.type ?? 'unknown'} confidence=${d.confidence ?? 'unknown'} risk=${d.riskLevel ?? 'unknown'}` : '', autoApplyAvailable
|
|
1905
|
+
? ` ✓ auto-applicable — call apply_broken_job_fix with jobName="${b.jobName}"`
|
|
1906
|
+
: ' ✗ not auto-applicable — manual review or dashboard intervention needed');
|
|
1907
|
+
}
|
|
1908
|
+
return textResult(lines.filter(Boolean).join('\n'));
|
|
1909
|
+
});
|
|
1910
|
+
server.tool('apply_broken_job_fix', 'Apply the cached auto-applicable fix for a broken cron job. Use this when the user explicitly asks to "fix" a job that has a confirmed diagnosis with autoApply=true and risk=low. Pass dryRun=true to preview without writing. Returns the applied operations, or refuses with a clear reason when the diagnosis is missing/risky/non-auto-applicable.', {
|
|
1911
|
+
jobName: z.string().describe('The job name as shown in CRON.md or list_broken_jobs output (e.g. "audit-inbox-check" or "ross-the-sdr:reply-detection").'),
|
|
1912
|
+
dryRun: z.boolean().optional().describe('If true, validate + show what would change but do not write. Default false.'),
|
|
1913
|
+
}, async ({ jobName, dryRun }) => {
|
|
1914
|
+
const { getDiagnosisIfFresh, clearDiagnosis } = await import('../gateway/failure-diagnostics.js');
|
|
1915
|
+
const { applyFix } = await import('../gateway/fix-applier.js');
|
|
1916
|
+
const d = getDiagnosisIfFresh(jobName);
|
|
1917
|
+
if (!d) {
|
|
1918
|
+
return textResult(`No fresh diagnosis for \`${jobName}\`. The failure-monitor sweep hasn't produced one yet, ` +
|
|
1919
|
+
`or the diagnosis expired. Wait for the next sweep, or dig into ~/.clementine/cron/runs/${jobName}.jsonl ` +
|
|
1920
|
+
`and the run-trace files for the actual error.`);
|
|
1921
|
+
}
|
|
1922
|
+
if (!d.proposedFix?.autoApply) {
|
|
1923
|
+
return textResult(`Diagnosis for \`${jobName}\` has no auto-applicable operations. ` +
|
|
1924
|
+
`Type: ${d.proposedFix?.type ?? 'unknown'}. ` +
|
|
1925
|
+
`This usually means the fix needs manual review — surface the diagnosis to the owner ` +
|
|
1926
|
+
`(${d.rootCause ?? "(no root cause)"}) instead of attempting auto-fix.`);
|
|
1927
|
+
}
|
|
1928
|
+
if (d.riskLevel !== 'low') {
|
|
1929
|
+
return textResult(`Diagnosis for \`${jobName}\` has riskLevel=${d.riskLevel}. ` +
|
|
1930
|
+
`Auto-apply is gated to risk=low only. ` +
|
|
1931
|
+
`Show the proposed fix to the owner for explicit approval.`);
|
|
1932
|
+
}
|
|
1933
|
+
const isDryRun = dryRun === true;
|
|
1934
|
+
const result = applyFix(jobName, d.proposedFix.autoApply, { dryRun: isDryRun });
|
|
1935
|
+
if (result.ok && !isDryRun)
|
|
1936
|
+
clearDiagnosis(jobName);
|
|
1937
|
+
if (!result.ok) {
|
|
1938
|
+
return textResult(`Apply failed for \`${jobName}\`: ${'error' in result ? result.error : 'unknown error'}`);
|
|
1939
|
+
}
|
|
1940
|
+
const opsCount = 'operations' in result ? result.operations.length : 0;
|
|
1941
|
+
return textResult(isDryRun
|
|
1942
|
+
? `[DRY RUN] Would apply ${opsCount} operation${opsCount === 1 ? '' : 's'} to fix \`${jobName}\`. ` +
|
|
1943
|
+
`Root cause: ${d.rootCause?.slice(0, 200) ?? ""}. Re-run without dryRun to commit.`
|
|
1944
|
+
: `Applied fix for \`${jobName}\` (${opsCount} operation${opsCount === 1 ? '' : 's'}). ` +
|
|
1945
|
+
`The fix-verification tracker will roll it back automatically if the next runs don't improve. ` +
|
|
1946
|
+
`Root cause: ${d.rootCause?.slice(0, 200) ?? ""}.`);
|
|
1947
|
+
});
|
|
1884
1948
|
}
|
|
1885
1949
|
//# sourceMappingURL=admin-tools.js.map
|