@compilr-dev/sdk 0.17.18 → 0.18.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/dist/team/consult-tool.d.ts +33 -7
- package/dist/team/consult-tool.js +30 -3
- package/package.json +1 -1
|
@@ -40,31 +40,57 @@ export interface ConsultResult {
|
|
|
40
40
|
* (target run exceeded max iterations, transport error, etc.). */
|
|
41
41
|
error?: string;
|
|
42
42
|
}
|
|
43
|
+
/** Default wall-clock cap for a whole consult sub-run (ms). */
|
|
44
|
+
export declare const DEFAULT_CONSULT_TIMEOUT_MS = 180000;
|
|
45
|
+
/**
|
|
46
|
+
* Options passed to `onConsult` alongside the question. Additive — hosts
|
|
47
|
+
* that predate the timeout work can ignore `signal` and keep compiling,
|
|
48
|
+
* but they will NOT get the hang protection until they forward it into
|
|
49
|
+
* their target `agent.run(..., { signal })`.
|
|
50
|
+
*/
|
|
51
|
+
export interface ConsultRunOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Aborts when the wall-clock timeout fires (or the host links its own
|
|
54
|
+
* borrower signal to it). Implementations MUST forward this into the
|
|
55
|
+
* target agent's run so an expired consult actually stops the sub-run
|
|
56
|
+
* instead of orphaning it.
|
|
57
|
+
*/
|
|
58
|
+
signal: AbortSignal;
|
|
59
|
+
}
|
|
43
60
|
/**
|
|
44
61
|
* Configuration for `createConsultTool`.
|
|
45
62
|
*
|
|
46
63
|
* `onConsult` is intentionally platform-shaped — each host runs sub-agents
|
|
47
64
|
* differently (CLI's REPL sub-context, Desktop's agent-manager, in-process
|
|
48
|
-
* Agent.run). The SDK owns validation
|
|
49
|
-
* owns the actual run.
|
|
65
|
+
* Agent.run). The SDK owns validation, message format, and the wall-clock
|
|
66
|
+
* timeout; the platform owns the actual run.
|
|
50
67
|
*/
|
|
51
68
|
export interface ConsultToolConfig {
|
|
52
69
|
/** The team instance — used to validate target agent membership. */
|
|
53
70
|
team: AgentTeam;
|
|
54
71
|
/** The borrowing agent's ID. */
|
|
55
72
|
currentAgentId: string;
|
|
73
|
+
/**
|
|
74
|
+
* Wall-clock cap for the whole consult sub-run, in ms. The SDK arms a
|
|
75
|
+
* timer, aborts `signal` on expiry, and returns a recoverable timeout
|
|
76
|
+
* error to the borrower's LLM. Defaults to {@link DEFAULT_CONSULT_TIMEOUT_MS}.
|
|
77
|
+
*/
|
|
78
|
+
timeoutMs?: number;
|
|
56
79
|
/**
|
|
57
80
|
* Platform-specific consult handler. Called after validation passes.
|
|
58
81
|
* Should:
|
|
59
82
|
* 1. Synthesise the question message (use `buildConsultQuestionMessage`).
|
|
60
|
-
* 2. Run the target agent against that message
|
|
61
|
-
*
|
|
83
|
+
* 2. Run the target agent against that message, **history-aware** (the
|
|
84
|
+
* target's real agent/history — NOT a throwaway or snapshot-restored
|
|
85
|
+
* run), forwarding `opts.signal` into the run so the timeout can
|
|
86
|
+
* abort it.
|
|
62
87
|
* 3. Return the target's final response text.
|
|
63
88
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
89
|
+
* As of consult v2 the target DOES remember the exchange — the historical
|
|
90
|
+
* "transient / history not mutated" contract is removed. See
|
|
91
|
+
* `consult-persistent-conversation-spec.md`.
|
|
66
92
|
*/
|
|
67
|
-
onConsult: (targetAgentId: string, question: string, context: string | undefined) => Promise<{
|
|
93
|
+
onConsult: (targetAgentId: string, question: string, context: string | undefined, opts: ConsultRunOptions) => Promise<{
|
|
68
94
|
answer: string;
|
|
69
95
|
error?: string;
|
|
70
96
|
}>;
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* /workspace/project-docs/00-requirements/compilr-dev-sdk/consult-tool-spec.md
|
|
22
22
|
*/
|
|
23
23
|
import { defineTool } from '@compilr-dev/agents';
|
|
24
|
+
/** Default wall-clock cap for a whole consult sub-run (ms). */
|
|
25
|
+
export const DEFAULT_CONSULT_TIMEOUT_MS = 180_000;
|
|
24
26
|
// =============================================================================
|
|
25
27
|
// Canonical question-message format
|
|
26
28
|
// =============================================================================
|
|
@@ -63,10 +65,12 @@ export function buildConsultQuestionMessage(input) {
|
|
|
63
65
|
*/
|
|
64
66
|
export function createConsultTool(config) {
|
|
65
67
|
const { team, currentAgentId, onConsult } = config;
|
|
68
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_CONSULT_TIMEOUT_MS;
|
|
66
69
|
return defineTool({
|
|
67
70
|
name: 'consult',
|
|
68
|
-
description: 'Ask
|
|
69
|
-
'
|
|
71
|
+
description: 'Ask a teammate a focused question and continue your own work. ' +
|
|
72
|
+
'They answer using their own context and remember the exchange, so a consult is a real interaction in their conversation — not a throwaway. ' +
|
|
73
|
+
'Use this when you need a quick opinion or expertise from a teammate without giving up the conversation. ' +
|
|
70
74
|
'Unlike handoff, you keep ownership — the target answers and control returns to you. ' +
|
|
71
75
|
'Example: $arch can consult $pm on scope, get an answer, and continue designing.',
|
|
72
76
|
inputSchema: {
|
|
@@ -123,8 +127,19 @@ export function createConsultTool(config) {
|
|
|
123
127
|
error: `Agent "${targetId}" not found in team. Available specialists: ${available || '(none)'}`,
|
|
124
128
|
};
|
|
125
129
|
}
|
|
130
|
+
// Wall-clock cap for the whole sub-run. The SDK owns this so BOTH
|
|
131
|
+
// hosts inherit the hang fix without duplicating timeout logic. On
|
|
132
|
+
// expiry we abort the signal (the host forwards it into the target
|
|
133
|
+
// run) and hand the borrower's LLM a recoverable error rather than
|
|
134
|
+
// hanging "running" forever — the failure mode observed in testing.
|
|
135
|
+
const ac = new AbortController();
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
ac.abort(new Error('consult-timeout'));
|
|
138
|
+
}, timeoutMs);
|
|
126
139
|
try {
|
|
127
|
-
const { answer, error } = await onConsult(targetId, question, context
|
|
140
|
+
const { answer, error } = await onConsult(targetId, question, context, {
|
|
141
|
+
signal: ac.signal,
|
|
142
|
+
});
|
|
128
143
|
if (error) {
|
|
129
144
|
return { success: false, error };
|
|
130
145
|
}
|
|
@@ -132,11 +147,23 @@ export function createConsultTool(config) {
|
|
|
132
147
|
return { success: true, result };
|
|
133
148
|
}
|
|
134
149
|
catch (err) {
|
|
150
|
+
if (ac.signal.aborted) {
|
|
151
|
+
const seconds = String(Math.round(timeoutMs / 1000));
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: `$${targetId} didn't respond in time (${seconds}s). ` +
|
|
155
|
+
'It may have a pending question — the request was surfaced in your chat. ' +
|
|
156
|
+
'You can try consulting again, ask the user, or proceed without it.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
135
159
|
return {
|
|
136
160
|
success: false,
|
|
137
161
|
error: `Consult failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
138
162
|
};
|
|
139
163
|
}
|
|
164
|
+
finally {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
}
|
|
140
167
|
},
|
|
141
168
|
});
|
|
142
169
|
}
|