@desplega.ai/agent-swarm 1.71.2 → 1.72.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.
Files changed (62) hide show
  1. package/README.md +3 -2
  2. package/openapi.json +994 -62
  3. package/package.json +2 -1
  4. package/src/be/budget-admission.ts +121 -0
  5. package/src/be/budget-refusal-notify.ts +145 -0
  6. package/src/be/db.ts +488 -5
  7. package/src/be/migrations/044_provider_meta.sql +2 -0
  8. package/src/be/migrations/046_budgets_and_pricing.sql +87 -0
  9. package/src/be/migrations/047_session_costs_cost_source.sql +16 -0
  10. package/src/cli.tsx +22 -1
  11. package/src/commands/claude-managed-setup.ts +687 -0
  12. package/src/commands/codex-login.ts +1 -1
  13. package/src/commands/runner.ts +175 -28
  14. package/src/commands/templates.ts +10 -6
  15. package/src/http/budgets.ts +219 -0
  16. package/src/http/index.ts +6 -0
  17. package/src/http/integrations.ts +134 -0
  18. package/src/http/poll.ts +161 -3
  19. package/src/http/pricing.ts +245 -0
  20. package/src/http/session-data.ts +54 -6
  21. package/src/http/tasks.ts +23 -2
  22. package/src/prompts/base-prompt.ts +103 -73
  23. package/src/prompts/session-templates.ts +43 -0
  24. package/src/providers/claude-adapter.ts +3 -1
  25. package/src/providers/claude-managed-adapter.ts +871 -0
  26. package/src/providers/claude-managed-models.ts +117 -0
  27. package/src/providers/claude-managed-swarm-events.ts +77 -0
  28. package/src/providers/codex-adapter.ts +3 -1
  29. package/src/providers/codex-skill-resolver.ts +10 -0
  30. package/src/providers/codex-swarm-events.ts +20 -161
  31. package/src/providers/devin-adapter.ts +894 -0
  32. package/src/providers/devin-api.ts +207 -0
  33. package/src/providers/devin-playbooks.ts +91 -0
  34. package/src/providers/devin-skill-resolver.ts +113 -0
  35. package/src/providers/index.ts +10 -1
  36. package/src/providers/pi-mono-adapter.ts +3 -1
  37. package/src/providers/swarm-events-shared.ts +262 -0
  38. package/src/providers/types.ts +26 -1
  39. package/src/tests/base-prompt.test.ts +199 -0
  40. package/src/tests/budget-admission.test.ts +339 -0
  41. package/src/tests/budget-claim-gate.test.ts +288 -0
  42. package/src/tests/budget-refusal-notification.test.ts +324 -0
  43. package/src/tests/budgets-routes.test.ts +331 -0
  44. package/src/tests/claude-managed-adapter.test.ts +1301 -0
  45. package/src/tests/claude-managed-setup.test.ts +325 -0
  46. package/src/tests/devin-adapter.test.ts +677 -0
  47. package/src/tests/devin-api.test.ts +339 -0
  48. package/src/tests/integrations-http.test.ts +211 -0
  49. package/src/tests/migration-046-budgets.test.ts +327 -0
  50. package/src/tests/pricing-routes.test.ts +315 -0
  51. package/src/tests/prompt-template-remaining.test.ts +4 -0
  52. package/src/tests/prompt-template-session.test.ts +2 -2
  53. package/src/tests/provider-adapter.test.ts +1 -1
  54. package/src/tests/runner-budget-refused.test.ts +271 -0
  55. package/src/tests/session-costs-codex-recompute.test.ts +386 -0
  56. package/src/tools/poll-task.ts +13 -2
  57. package/src/tools/task-action.ts +92 -2
  58. package/src/tools/templates.ts +29 -0
  59. package/src/types.ts +116 -0
  60. package/src/utils/budget-backoff.ts +34 -0
  61. package/src/utils/credentials.ts +4 -0
  62. package/src/utils/provider-metadata.ts +9 -0
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import {
4
4
  createSessionCost,
5
5
  createSessionLogs,
6
+ getActivePricingRow,
6
7
  getAllSessionCosts,
7
8
  getDashboardCostSummary,
8
9
  getSessionCostSummary,
@@ -12,7 +13,7 @@ import {
12
13
  getSessionLogsByTaskId,
13
14
  getTaskById,
14
15
  } from "../be/db";
15
- import type { SessionCost } from "../types";
16
+ import type { SessionCost, SessionCostSource } from "../types";
16
17
  import { route } from "./route-def";
17
18
  import { json, jsonError } from "./utils";
18
19
 
@@ -69,6 +70,18 @@ const createSessionCostRoute = route({
69
70
  numTurns: z.number().int().optional(),
70
71
  model: z.string().optional(),
71
72
  isError: z.boolean().optional(),
73
+ /**
74
+ * Phase 6: when present, drives the codex pricing-table recompute path.
75
+ * Other providers ('claude' / 'pi') always trust harness-reported USD.
76
+ * Optional / undefined keeps back-compat for existing callers.
77
+ */
78
+ provider: z.enum(["claude", "codex", "pi"]).optional(),
79
+ /**
80
+ * Phase 6: epoch-ms timestamp used as the "active price at time T" lookup
81
+ * basis. Defaults to `Date.now()` when omitted. Including it lets
82
+ * historical recomputes pick the correct `effective_from` row.
83
+ */
84
+ createdAt: z.number().int().nonnegative().optional(),
72
85
  }),
73
86
  responses: {
74
87
  201: { description: "Cost record stored" },
@@ -170,19 +183,54 @@ export async function handleSessionData(
170
183
  if (!parsed) return true;
171
184
 
172
185
  try {
186
+ const inputTokens = parsed.body.inputTokens ?? 0;
187
+ const cachedInputTokens = parsed.body.cacheReadTokens ?? 0;
188
+ const outputTokens = parsed.body.outputTokens ?? 0;
189
+ const model = parsed.body.model || "opus";
190
+
191
+ // Phase 6: Codex USD recompute. When the worker reports `provider='codex'`
192
+ // and DB pricing rows exist for ALL three token classes at the lookup
193
+ // time, recompute `totalCostUsd` from tokens × DB prices and tag the
194
+ // row as 'pricing-table'. If any class has no row, fall back to the
195
+ // worker-reported value with `costSource='harness'` (back-compat for
196
+ // unseeded models). Claude / pi paths always use 'harness'.
197
+ let totalCostUsd = parsed.body.totalCostUsd;
198
+ let costSource: SessionCostSource = "harness";
199
+
200
+ if (parsed.body.provider === "codex") {
201
+ const lookupTime = parsed.body.createdAt ?? Date.now();
202
+ const inputRow = getActivePricingRow("codex", model, "input", lookupTime);
203
+ const cachedRow = getActivePricingRow("codex", model, "cached_input", lookupTime);
204
+ const outputRow = getActivePricingRow("codex", model, "output", lookupTime);
205
+
206
+ if (inputRow && cachedRow && outputRow) {
207
+ // Mirror the existing computeCodexCostUsd logic: subtract cached
208
+ // tokens from input before billing the uncached portion at the full
209
+ // rate (Codex SDK reports input_tokens as TOTAL across the turn).
210
+ const uncachedInputTokens = Math.max(0, inputTokens - cachedInputTokens);
211
+ totalCostUsd =
212
+ (uncachedInputTokens * inputRow.pricePerMillionUsd +
213
+ cachedInputTokens * cachedRow.pricePerMillionUsd +
214
+ outputTokens * outputRow.pricePerMillionUsd) /
215
+ 1_000_000;
216
+ costSource = "pricing-table";
217
+ }
218
+ }
219
+
173
220
  const cost = createSessionCost({
174
221
  sessionId: parsed.body.sessionId,
175
222
  taskId: parsed.body.taskId || undefined,
176
223
  agentId: parsed.body.agentId,
177
- totalCostUsd: parsed.body.totalCostUsd,
178
- inputTokens: parsed.body.inputTokens ?? 0,
179
- outputTokens: parsed.body.outputTokens ?? 0,
180
- cacheReadTokens: parsed.body.cacheReadTokens ?? 0,
224
+ totalCostUsd,
225
+ inputTokens,
226
+ outputTokens,
227
+ cacheReadTokens: cachedInputTokens,
181
228
  cacheWriteTokens: parsed.body.cacheWriteTokens ?? 0,
182
229
  durationMs: parsed.body.durationMs ?? 0,
183
230
  numTurns: parsed.body.numTurns ?? 1,
184
- model: parsed.body.model || "opus",
231
+ model,
185
232
  isError: parsed.body.isError ?? false,
233
+ costSource,
186
234
  });
187
235
  json(res, { success: true, cost }, 201);
188
236
  } catch (error) {
package/src/http/tasks.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  } from "../be/db";
21
21
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
22
22
  import { telemetry } from "../telemetry";
23
+ import { ProviderNameSchema } from "../types";
23
24
  import { route } from "./route-def";
24
25
  import { json, jsonError } from "./utils";
25
26
 
@@ -78,7 +79,22 @@ const updateClaudeSession = route({
78
79
  summary: "Update Claude session ID for a task",
79
80
  tags: ["Tasks"],
80
81
  params: z.object({ id: z.string() }),
81
- body: z.object({ claudeSessionId: z.string().min(1) }),
82
+ body: z.union([
83
+ z.object({
84
+ claudeSessionId: z.string().min(1),
85
+ provider: z.literal("devin"),
86
+ providerMeta: z.object({
87
+ sessionUrl: z.string(),
88
+ maxAcuLimit: z.number().optional(),
89
+ acuCostUsd: z.number().optional(),
90
+ }),
91
+ }),
92
+ z.object({
93
+ claudeSessionId: z.string().min(1),
94
+ provider: ProviderNameSchema.exclude(["devin"]).optional(),
95
+ providerMeta: z.object({}).optional(),
96
+ }),
97
+ ]),
82
98
  responses: {
83
99
  200: { description: "Session ID updated" },
84
100
  404: { description: "Task not found" },
@@ -291,7 +307,12 @@ export async function handleTasks(
291
307
  if (updateClaudeSession.match(req.method, pathSegments)) {
292
308
  const parsed = await updateClaudeSession.parse(req, res, pathSegments, queryParams);
293
309
  if (!parsed) return true;
294
- const task = updateTaskClaudeSessionId(parsed.params.id, parsed.body.claudeSessionId);
310
+ const task = updateTaskClaudeSessionId(
311
+ parsed.params.id,
312
+ parsed.body.claudeSessionId,
313
+ parsed.body.provider,
314
+ parsed.body.providerMeta,
315
+ );
295
316
  if (!task) {
296
317
  jsonError(res, "Task not found", 404);
297
318
  return true;
@@ -7,6 +7,7 @@
7
7
  * still assembled here based on runtime state.
8
8
  */
9
9
 
10
+ import type { ProviderTraits } from "../providers/types";
10
11
  import { resolveTemplateAsync } from "./resolver";
11
12
 
12
13
  // Side-effect import: register all system + session templates
@@ -27,6 +28,7 @@ export type BasePromptArgs = {
27
28
  agentId: string;
28
29
  swarmUrl: string;
29
30
  capabilities?: string[];
31
+ traits?: ProviderTraits;
30
32
  name?: string;
31
33
  description?: string;
32
34
  soulMd?: string;
@@ -53,17 +55,25 @@ export type BasePromptArgs = {
53
55
  };
54
56
 
55
57
  export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
56
- const { role, agentId, swarmUrl } = args;
58
+ const { role, agentId, swarmUrl, traits } = args;
59
+ const { hasMcp = true, hasLocalEnvironment: hasLocalEnv = true } = traits ?? {};
57
60
 
58
61
  const vars: Record<string, string> = { role, agentId, swarmUrl };
59
62
 
60
- // Resolve the composite session template (lead or worker)
61
- const compositeEventType = role === "lead" ? "system.session.lead" : "system.session.worker";
63
+ // Resolve the composite session template (trait-aware for remote providers)
64
+ let compositeEventType: string;
65
+ if (!hasMcp) {
66
+ // If no MCP, role cannot be lead
67
+ compositeEventType = "system.session.worker.remote";
68
+ } else {
69
+ compositeEventType = role === "lead" ? "system.session.lead" : "system.session.worker";
70
+ }
62
71
  const compositeResult = await resolveTemplateAsync(compositeEventType, vars);
63
72
  let prompt = compositeResult.text;
64
73
 
65
74
  // Conditionally inject Slack instructions for workers with Slack-originated tasks
66
- if (role !== "lead" && args.slackContext) {
75
+ // Skip for providers without MCP — they can't call Slack tools (slack-reply, etc.)
76
+ if (role !== "lead" && args.slackContext && hasMcp) {
67
77
  const slackResult = await resolveTemplateAsync("system.agent.worker.slack", {
68
78
  slackChannelId: args.slackContext.channelId,
69
79
  slackThreadTs: args.slackContext.threadTs ?? "",
@@ -71,8 +81,21 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
71
81
  prompt += slackResult.text;
72
82
  }
73
83
 
74
- // Inject agent identity (soul + identity + name/description) if available
75
- if (args.soulMd || args.identityMd || args.name) {
84
+ // Inject agent identity
85
+ if (!hasLocalEnv) {
86
+ // Simplified identity for remote providers — no self-evolution, no /workspace files
87
+ prompt += "\n\n## Your Identity\n\n";
88
+ if (args.name) {
89
+ prompt += `**Name:** ${args.name}\n`;
90
+ if (args.description) {
91
+ prompt += `**Description:** ${args.description}\n`;
92
+ }
93
+ prompt += "\n";
94
+ }
95
+ prompt += `You are part of an agent swarm managed by the Desplega platform. `;
96
+ prompt += `You receive tasks from the swarm's lead agent and execute them independently. `;
97
+ prompt += `Focus on quality work and clear communication of results.\n`;
98
+ } else if (args.soulMd || args.identityMd || args.name) {
76
99
  prompt += "\n\n## Your Identity\n\n";
77
100
  if (args.name) {
78
101
  prompt += `**Name:** ${args.name}\n`;
@@ -90,13 +113,14 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
90
113
  }
91
114
 
92
115
  // Installed skills section (progressive disclosure — name + description only)
93
- if (args.skillsSummary && args.skillsSummary.length > 0) {
116
+ // Skip for providers without MCP — skills require the Skill MCP tool
117
+ if (hasMcp && args.skillsSummary && args.skillsSummary.length > 0) {
94
118
  const summaries = args.skillsSummary.map((s) => `- /${s.name}: ${s.description}`).join("\n");
95
119
  prompt += `\n\n## Installed Skills\n\nThe following skills are available. Use the Skill tool to invoke them by name.\n\n${summaries}\n`;
96
120
  }
97
121
 
98
- // Installed MCP servers section
99
- if (args.mcpServersSummary) {
122
+ // Installed MCP servers section — skip for providers without MCP
123
+ if (hasMcp && args.mcpServersSummary) {
100
124
  prompt += `\n\n## Installed MCP Servers\n\nThe following MCP servers are configured for your use:\n${args.mcpServersSummary}\n`;
101
125
  }
102
126
 
@@ -108,13 +132,15 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
108
132
  prompt += `WARNING: ${args.repoContext.warning}\n\n`;
109
133
  }
110
134
 
111
- if (args.repoContext.claudeMd) {
112
- prompt += `The following CLAUDE.md is from the repository cloned at \`${args.repoContext.clonePath}\`. `;
113
- prompt += `**IMPORTANT: These instructions apply ONLY when working within the \`${args.repoContext.clonePath}\` directory.** `;
114
- prompt += `Do NOT apply these rules to files outside that directory.\n\n`;
115
- prompt += `${args.repoContext.claudeMd}\n`;
116
- } else if (!args.repoContext.warning) {
117
- prompt += `Repository is cloned at \`${args.repoContext.clonePath}\` but has no CLAUDE.md file.\n`;
135
+ if (hasLocalEnv) {
136
+ if (args.repoContext.claudeMd) {
137
+ prompt += `The following CLAUDE.md is from the repository cloned at \`${args.repoContext.clonePath}\`. `;
138
+ prompt += `**IMPORTANT: These instructions apply ONLY when working within the \`${args.repoContext.clonePath}\` directory.** `;
139
+ prompt += `Do NOT apply these rules to files outside that directory.\n\n`;
140
+ prompt += `${args.repoContext.claudeMd}\n`;
141
+ } else if (!args.repoContext.warning) {
142
+ prompt += `Repository is cloned at \`${args.repoContext.clonePath}\` but has no CLAUDE.md file.\n`;
143
+ }
118
144
  }
119
145
 
120
146
  // Inject repo guidelines
@@ -153,73 +179,77 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
153
179
  }
154
180
  }
155
181
 
156
- // Build conditional suffix (sections that depend on runtime env/capabilities)
157
- let conditionalSuffix = "";
158
-
159
- // Conditionally include agent-fs instructions when available
160
- if (process.env.AGENT_FS_API_URL) {
161
- const sharedOrgId = process.env.AGENT_FS_SHARED_ORG_ID || "YOUR_SHARED_ORG_ID";
162
- const agentFsResult = await resolveTemplateAsync("system.agent.agent_fs", {
163
- agentId,
164
- sharedOrgId,
165
- });
166
- conditionalSuffix += agentFsResult.text;
167
- }
182
+ // Skip conditional suffix and truncatable sections for remote providers these
183
+ // reference local Docker environment features (agent-fs, services, artifacts, /workspace files)
184
+ if (hasLocalEnv) {
185
+ // Build conditional suffix (sections that depend on runtime env/capabilities)
186
+ let conditionalSuffix = "";
187
+
188
+ // Conditionally include agent-fs instructions when available
189
+ if (process.env.AGENT_FS_API_URL) {
190
+ const sharedOrgId = process.env.AGENT_FS_SHARED_ORG_ID || "YOUR_SHARED_ORG_ID";
191
+ const agentFsResult = await resolveTemplateAsync("system.agent.agent_fs", {
192
+ agentId,
193
+ sharedOrgId,
194
+ });
195
+ conditionalSuffix += agentFsResult.text;
196
+ }
168
197
 
169
- if (!args.capabilities || args.capabilities.includes("services")) {
170
- const servicesResult = await resolveTemplateAsync("system.agent.services", {
171
- agentId,
172
- swarmUrl,
173
- });
174
- conditionalSuffix += servicesResult.text;
175
- }
198
+ if (!args.capabilities || args.capabilities.includes("services")) {
199
+ const servicesResult = await resolveTemplateAsync("system.agent.services", {
200
+ agentId,
201
+ swarmUrl,
202
+ });
203
+ conditionalSuffix += servicesResult.text;
204
+ }
176
205
 
177
- if (!args.capabilities || args.capabilities.includes("artifacts")) {
178
- const artifactsResult = await resolveTemplateAsync("system.agent.artifacts", {});
179
- conditionalSuffix += artifactsResult.text;
180
- }
206
+ if (!args.capabilities || args.capabilities.includes("artifacts")) {
207
+ const artifactsResult = await resolveTemplateAsync("system.agent.artifacts", {});
208
+ conditionalSuffix += artifactsResult.text;
209
+ }
181
210
 
182
- if (args.capabilities) {
183
- conditionalSuffix += `
211
+ if (args.capabilities) {
212
+ conditionalSuffix += `
184
213
  ### Capabilities enabled for this agent:
185
214
 
186
215
  - ${args.capabilities.join("\n- ")}
187
216
  `;
188
- }
217
+ }
189
218
 
190
- // Inject truncatable sections with per-section and total character caps
191
- // Priority: agent CLAUDE.md > tools (tools cut first when over total budget)
192
- const protectedLength = prompt.length + conditionalSuffix.length;
193
- const totalBudget = Math.max(0, BOOTSTRAP_TOTAL_MAX_CHARS - protectedLength);
194
- let totalUsed = 0;
195
-
196
- // Agent CLAUDE.md (higher priority — injected first)
197
- if (args.claudeMd) {
198
- const perSectionBudget = Math.min(BOOTSTRAP_MAX_CHARS, totalBudget - totalUsed);
199
- const section = truncateSection(
200
- args.claudeMd,
201
- "## Agent Instructions",
202
- "CLAUDE.md",
203
- perSectionBudget,
204
- );
205
- prompt += section;
206
- totalUsed += section.length;
207
- }
219
+ // Inject truncatable sections with per-section and total character caps
220
+ // Priority: agent CLAUDE.md > tools (tools cut first when over total budget)
221
+ const protectedLength = prompt.length + conditionalSuffix.length;
222
+ const totalBudget = Math.max(0, BOOTSTRAP_TOTAL_MAX_CHARS - protectedLength);
223
+ let totalUsed = 0;
224
+
225
+ // Agent CLAUDE.md (higher priority — injected first)
226
+ if (args.claudeMd) {
227
+ const perSectionBudget = Math.min(BOOTSTRAP_MAX_CHARS, totalBudget - totalUsed);
228
+ const section = truncateSection(
229
+ args.claudeMd,
230
+ "## Agent Instructions",
231
+ "CLAUDE.md",
232
+ perSectionBudget,
233
+ );
234
+ prompt += section;
235
+ totalUsed += section.length;
236
+ }
208
237
 
209
- // Tools (lower priority — gets whatever budget remains)
210
- if (args.toolsMd) {
211
- const perSectionBudget = Math.min(BOOTSTRAP_MAX_CHARS, totalBudget - totalUsed);
212
- const section = truncateSection(
213
- args.toolsMd,
214
- "## Your Tools & Capabilities",
215
- "TOOLS.md",
216
- perSectionBudget,
217
- );
218
- prompt += section;
219
- totalUsed += section.length;
220
- }
238
+ // Tools (lower priority — gets whatever budget remains)
239
+ if (args.toolsMd) {
240
+ const perSectionBudget = Math.min(BOOTSTRAP_MAX_CHARS, totalBudget - totalUsed);
241
+ const section = truncateSection(
242
+ args.toolsMd,
243
+ "## Your Tools & Capabilities",
244
+ "TOOLS.md",
245
+ perSectionBudget,
246
+ );
247
+ prompt += section;
248
+ totalUsed += section.length;
249
+ }
221
250
 
222
- prompt += conditionalSuffix;
251
+ prompt += conditionalSuffix;
252
+ }
223
253
 
224
254
  return prompt;
225
255
  };
@@ -520,3 +520,46 @@ registerTemplate({
520
520
  ],
521
521
  category: "session",
522
522
  });
523
+
524
+ // ============================================================================
525
+ // Remote provider templates (no MCP, no Docker container)
526
+ // ============================================================================
527
+
528
+ registerTemplate({
529
+ eventType: "system.agent.worker.remote",
530
+ header: "",
531
+ defaultBody: `
532
+ As a worker agent of the swarm, you are responsible for executing tasks assigned by the lead agent.
533
+
534
+ - Each worker focuses on specific tasks or objectives, contributing to the overall goals of the swarm.
535
+ - Workers MUST report their progress back to the lead and collaborate with other workers as needed.
536
+
537
+ #### How Tasks Work
538
+
539
+ You receive tasks via the session prompt. Each task has a description of what needs to be done.
540
+
541
+ #### Completing Tasks
542
+
543
+ When you finish a task:
544
+ - Provide a clear summary of what you accomplished in your final message
545
+ - If you created a PR, include the PR URL
546
+ - If you encountered blockers, explain what blocked you and what you tried
547
+
548
+ Your output is captured automatically — focus on doing the work and communicating results clearly.
549
+ `,
550
+ variables: [],
551
+ category: "system",
552
+ });
553
+
554
+ registerTemplate({
555
+ eventType: "system.session.worker.remote",
556
+ header: "",
557
+ defaultBody: `{{@template[system.agent.role]}}
558
+
559
+ {{@template[system.agent.worker.remote]}}`,
560
+ variables: [
561
+ { name: "role", description: "The agent's role" },
562
+ { name: "agentId", description: "The agent's unique identifier" },
563
+ ],
564
+ category: "session",
565
+ });
@@ -394,7 +394,7 @@ class ClaudeSession implements ProviderSession {
394
394
  // Session ID from init message
395
395
  if (json.type === "system" && json.subtype === "init" && json.session_id) {
396
396
  this._sessionId = json.session_id;
397
- this.emit({ type: "session_init", sessionId: json.session_id });
397
+ this.emit({ type: "session_init", sessionId: json.session_id, provider: "claude" });
398
398
  if (json.model) {
399
399
  this.contextWindowSize = getContextWindowSize(json.model);
400
400
  }
@@ -434,6 +434,7 @@ class ClaudeSession implements ProviderSession {
434
434
  numTurns: json.num_turns || 1,
435
435
  model: this.model,
436
436
  isError: json.is_error || false,
437
+ provider: "claude",
437
438
  };
438
439
  setCost(cost);
439
440
  this.emit({
@@ -568,6 +569,7 @@ class ClaudeSession implements ProviderSession {
568
569
 
569
570
  export class ClaudeAdapter implements ProviderAdapter {
570
571
  readonly name = "claude";
572
+ readonly traits = { hasMcp: true, hasLocalEnvironment: true };
571
573
 
572
574
  async createSession(config: ProviderSessionConfig): Promise<ProviderSession> {
573
575
  const model = config.model || "opus";