@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.
@@ -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 and message format; the platform
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 (sub-run, not a turn
61
- * in the borrower's conversation).
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
- * The target's conversation history is NOT mutated by a consult — the
65
- * call is transient.
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 another specialist a focused question and continue the conversation yourself. ' +
69
- 'Use this when you need a quick opinion or expertise input from a teammate without giving up the conversation. ' +
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.17.18",
3
+ "version": "0.18.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",