@modelrelay/sdk 5.1.0 → 8.0.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/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  APIError,
3
+ AgentMaxTurnsError,
3
4
  BillingProviders,
4
5
  ConfigError,
5
6
  ContentPartTypes,
@@ -26,7 +27,6 @@ import {
26
27
  ToolRegistry,
27
28
  ToolTypes,
28
29
  TransportError,
29
- WebToolIntents,
30
30
  WorkflowValidationError,
31
31
  asModelId,
32
32
  asProviderId,
@@ -39,13 +39,20 @@ import {
39
39
  createRetryMessages,
40
40
  createSystemMessage,
41
41
  createToolCall,
42
+ createTypedTool,
42
43
  createUsage,
43
44
  createUserMessage,
44
- createWebTool,
45
45
  executeWithRetry,
46
46
  firstToolCall,
47
47
  formatToolErrorForModel,
48
+ getAllToolCalls,
49
+ getAssistantText,
48
50
  getRetryableErrors,
51
+ getToolArgs,
52
+ getToolArgsRaw,
53
+ getToolName,
54
+ getTypedToolCall,
55
+ getTypedToolCalls,
49
56
  hasRetryableErrors,
50
57
  hasToolCalls,
51
58
  mergeMetrics,
@@ -54,17 +61,15 @@ import {
54
61
  normalizeModelId,
55
62
  normalizeStopReason,
56
63
  parseErrorResponse,
57
- parseToolArgs,
58
- parseToolArgsRaw,
64
+ parseTypedToolCall,
59
65
  respondToToolCall,
60
66
  stopReasonToString,
61
67
  toolChoiceAuto,
62
68
  toolChoiceNone,
63
69
  toolChoiceRequired,
64
70
  toolResultMessage,
65
- tryParseToolArgs,
66
71
  zodToJsonSchema
67
- } from "./chunk-RVHKBQ7X.js";
72
+ } from "./chunk-SJC7Q4NK.js";
68
73
  import {
69
74
  __export
70
75
  } from "./chunk-MLKGABMK.js";
@@ -154,6 +159,9 @@ var AuthClient = class {
154
159
  if (typeof request.ttlSeconds === "number") {
155
160
  payload.ttl_seconds = request.ttlSeconds;
156
161
  }
162
+ if (request.tierCode) {
163
+ payload.tier_code = request.tierCode;
164
+ }
157
165
  const apiResp = await this.http.json("/auth/customer-token", {
158
166
  method: "POST",
159
167
  body: payload,
@@ -209,7 +217,8 @@ var AuthClient = class {
209
217
  });
210
218
  return this.customerToken({
211
219
  customerExternalId: externalId,
212
- ttlSeconds: request.ttlSeconds
220
+ ttlSeconds: request.ttlSeconds,
221
+ tierCode: request.tierCode
213
222
  });
214
223
  }
215
224
  /**
@@ -327,13 +336,38 @@ var ResponseBuilder = class _ResponseBuilder {
327
336
  patch.options ? { ...this.options, ...patch.options } : this.options
328
337
  );
329
338
  }
330
- /** @returns A new builder with the provider set. */
339
+ /**
340
+ * Set the provider for this request.
341
+ *
342
+ * Accepts either a string or ProviderId for convenience.
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * .provider("anthropic") // String works
347
+ * .provider(asProviderId("anthropic")) // ProviderId also works
348
+ * ```
349
+ */
331
350
  provider(provider) {
332
- return this.with({ body: { provider } });
351
+ return this.with({ body: { provider: asProviderId(provider) } });
333
352
  }
334
- /** @returns A new builder with the model set. */
353
+ /**
354
+ * Set the model for this request.
355
+ *
356
+ * Accepts either a string or ModelId for convenience.
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * .model("claude-sonnet-4-5") // String works
361
+ * .model(asModelId("claude-sonnet-4-5")) // ModelId also works
362
+ * ```
363
+ */
335
364
  model(model) {
336
- return this.with({ body: { model } });
365
+ return this.with({ body: { model: asModelId(model) } });
366
+ }
367
+ /** @returns A new builder with state-scoped tool state. */
368
+ stateId(stateId) {
369
+ const state_id = stateId.trim();
370
+ return this.with({ body: { state_id: state_id || void 0 } });
337
371
  }
338
372
  /** @returns A new builder with the full input array replaced. */
339
373
  input(items) {
@@ -481,6 +515,111 @@ var ResponseBuilder = class _ResponseBuilder {
481
515
  signal(signal) {
482
516
  return this.with({ options: { signal } });
483
517
  }
518
+ // =========================================================================
519
+ // Conversation Continuation Helpers
520
+ // =========================================================================
521
+ /**
522
+ * Add an assistant message with tool calls from a previous response.
523
+ *
524
+ * This is useful for continuing a conversation after handling tool calls.
525
+ *
526
+ * @example
527
+ * ```typescript
528
+ * const response = await mr.responses.create(request);
529
+ * if (hasToolCalls(response)) {
530
+ * const toolCalls = response.output[0].toolCalls!;
531
+ * const results = await registry.executeAll(toolCalls);
532
+ *
533
+ * const followUp = await mr.responses.create(
534
+ * mr.responses.new()
535
+ * .model("claude-sonnet-4-5")
536
+ * .user("What's the weather in Paris?")
537
+ * .assistantToolCalls(toolCalls)
538
+ * .toolResults(results.map(r => ({ id: r.toolCallId, result: r.result })))
539
+ * .build()
540
+ * );
541
+ * }
542
+ * ```
543
+ */
544
+ assistantToolCalls(toolCalls, content) {
545
+ return this.item(assistantMessageWithToolCalls(content ?? "", toolCalls));
546
+ }
547
+ /**
548
+ * Add tool results to the conversation.
549
+ *
550
+ * @example
551
+ * ```typescript
552
+ * .toolResults([
553
+ * { id: "call_123", result: { temp: 72 } },
554
+ * { id: "call_456", result: "File contents here" },
555
+ * ])
556
+ * ```
557
+ */
558
+ toolResults(results) {
559
+ let builder = this;
560
+ for (const r of results) {
561
+ const content = typeof r.result === "string" ? r.result : JSON.stringify(r.result);
562
+ builder = builder.item(toolResultMessage(r.id, content));
563
+ }
564
+ return builder;
565
+ }
566
+ /**
567
+ * Add a single tool result to the conversation.
568
+ *
569
+ * @example
570
+ * ```typescript
571
+ * .toolResult("call_123", { temp: 72, unit: "fahrenheit" })
572
+ * ```
573
+ */
574
+ toolResult(toolCallId, result) {
575
+ const content = typeof result === "string" ? result : JSON.stringify(result);
576
+ return this.item(toolResultMessage(toolCallId, content));
577
+ }
578
+ /**
579
+ * Continue from a previous response that contains tool calls.
580
+ *
581
+ * This is the most ergonomic way to continue a conversation after handling tools.
582
+ * It automatically adds the assistant's tool call message and your tool results.
583
+ *
584
+ * @example
585
+ * ```typescript
586
+ * const response = await mr.responses.create(request);
587
+ *
588
+ * if (hasToolCalls(response)) {
589
+ * const toolCalls = response.output[0].toolCalls!;
590
+ * const results = await registry.executeAll(toolCalls);
591
+ *
592
+ * const followUp = await mr.responses.create(
593
+ * mr.responses.new()
594
+ * .model("claude-sonnet-4-5")
595
+ * .tools(myTools)
596
+ * .user("What's the weather in Paris?")
597
+ * .continueFrom(response, results.map(r => ({
598
+ * id: r.toolCallId,
599
+ * result: r.result,
600
+ * })))
601
+ * .build()
602
+ * );
603
+ * }
604
+ * ```
605
+ */
606
+ continueFrom(response, toolResults) {
607
+ const toolCalls = [];
608
+ for (const item of response.output || []) {
609
+ if (item.toolCalls) {
610
+ toolCalls.push(...item.toolCalls);
611
+ }
612
+ }
613
+ if (toolCalls.length === 0) {
614
+ throw new ConfigError(
615
+ "continueFrom requires a response with tool calls"
616
+ );
617
+ }
618
+ const assistantText = getAssistantText(response);
619
+ return this.assistantToolCalls(toolCalls, assistantText).toolResults(
620
+ toolResults
621
+ );
622
+ }
484
623
  /** @returns A finalized, immutable request payload. */
485
624
  build() {
486
625
  const input = (this.body.input ?? []).slice();
@@ -490,6 +629,7 @@ var ResponseBuilder = class _ResponseBuilder {
490
629
  const body = {
491
630
  provider: this.body.provider,
492
631
  model: this.body.model,
632
+ state_id: this.body.state_id,
493
633
  input,
494
634
  output_format: this.body.output_format,
495
635
  max_output_tokens: this.body.max_output_tokens,
@@ -1806,7 +1946,7 @@ function parseOutputName(raw) {
1806
1946
  var WorkflowKinds = {
1807
1947
  WorkflowIntent: "workflow"
1808
1948
  };
1809
- var WorkflowNodeTypesLite = {
1949
+ var WorkflowNodeTypesIntent = {
1810
1950
  LLM: "llm",
1811
1951
  JoinAll: "join.all",
1812
1952
  JoinAny: "join.any",
@@ -1876,6 +2016,25 @@ var nodeWaitingSchema = z.object({
1876
2016
  pending_tool_calls: z.array(pendingToolCallSchema).min(1),
1877
2017
  reason: z.string().min(1)
1878
2018
  }).strict();
2019
+ var userAskOptionSchema = z.object({
2020
+ label: z.string().min(1),
2021
+ description: z.string().optional()
2022
+ }).strict();
2023
+ var nodeUserAskSchema = z.object({
2024
+ step: z.number().int().nonnegative(),
2025
+ request_id: z.string().min(1),
2026
+ tool_call: toolCallWithArgumentsSchema,
2027
+ question: z.string().min(1),
2028
+ options: z.array(userAskOptionSchema).optional(),
2029
+ allow_freeform: z.boolean()
2030
+ }).strict();
2031
+ var nodeUserAnswerSchema = z.object({
2032
+ step: z.number().int().nonnegative(),
2033
+ request_id: z.string().min(1),
2034
+ tool_call: toolCallSchema,
2035
+ answer: z.string(),
2036
+ is_freeform: z.boolean()
2037
+ }).strict();
1879
2038
  var baseSchema = {
1880
2039
  envelope_version: z.literal("v2").optional().default("v2"),
1881
2040
  run_id: z.string().min(1),
@@ -1927,6 +2086,18 @@ var runEventWireSchema = z.discriminatedUnion("type", [
1927
2086
  node_id: z.string().min(1),
1928
2087
  waiting: nodeWaitingSchema
1929
2088
  }).strict(),
2089
+ z.object({
2090
+ ...baseSchema,
2091
+ type: z.literal("node_user_ask"),
2092
+ node_id: z.string().min(1),
2093
+ user_ask: nodeUserAskSchema
2094
+ }).strict(),
2095
+ z.object({
2096
+ ...baseSchema,
2097
+ type: z.literal("node_user_answer"),
2098
+ node_id: z.string().min(1),
2099
+ user_answer: nodeUserAnswerSchema
2100
+ }).strict(),
1930
2101
  z.object({ ...baseSchema, type: z.literal("node_started"), node_id: z.string().min(1) }).strict(),
1931
2102
  z.object({ ...baseSchema, type: z.literal("node_succeeded"), node_id: z.string().min(1) }).strict(),
1932
2103
  z.object({
@@ -2020,6 +2191,10 @@ function parseRunEventV0(line) {
2020
2191
  return { ...base, type: "node_tool_result", node_id: parseNodeId(res.data.node_id), tool_result: res.data.tool_result };
2021
2192
  case "node_waiting":
2022
2193
  return { ...base, type: "node_waiting", node_id: parseNodeId(res.data.node_id), waiting: res.data.waiting };
2194
+ case "node_user_ask":
2195
+ return { ...base, type: "node_user_ask", node_id: parseNodeId(res.data.node_id), user_ask: res.data.user_ask };
2196
+ case "node_user_answer":
2197
+ return { ...base, type: "node_user_answer", node_id: parseNodeId(res.data.node_id), user_answer: res.data.user_answer };
2023
2198
  case "node_succeeded":
2024
2199
  return { ...base, type: "node_succeeded", node_id: parseNodeId(res.data.node_id) };
2025
2200
  case "node_failed":
@@ -2181,6 +2356,90 @@ var RunsClient = class {
2181
2356
  if (options.input) {
2182
2357
  payload.input = options.input;
2183
2358
  }
2359
+ if (options.modelOverride?.trim()) {
2360
+ payload.model_override = options.modelOverride.trim();
2361
+ }
2362
+ if (options.modelOverrides) {
2363
+ const nodes = options.modelOverrides.nodes;
2364
+ const fanoutSubnodes = options.modelOverrides.fanoutSubnodes;
2365
+ if (nodes && Object.keys(nodes).length > 0 || fanoutSubnodes && fanoutSubnodes.length > 0) {
2366
+ payload.model_overrides = {
2367
+ nodes,
2368
+ fanout_subnodes: fanoutSubnodes?.map((entry) => ({
2369
+ parent_id: entry.parentId,
2370
+ subnode_id: entry.subnodeId,
2371
+ model: entry.model
2372
+ }))
2373
+ };
2374
+ }
2375
+ }
2376
+ if (options.stream !== void 0) {
2377
+ payload.stream = options.stream;
2378
+ }
2379
+ const out = await this.http.json(RUNS_PATH, {
2380
+ method: "POST",
2381
+ headers,
2382
+ body: payload,
2383
+ signal: options.signal,
2384
+ apiKey: authHeaders.apiKey,
2385
+ accessToken: authHeaders.accessToken,
2386
+ timeoutMs: options.timeoutMs,
2387
+ connectTimeoutMs: options.connectTimeoutMs,
2388
+ retry: options.retry,
2389
+ metrics,
2390
+ trace,
2391
+ context: { method: "POST", path: RUNS_PATH }
2392
+ });
2393
+ return { ...out, run_id: parseRunId(out.run_id), plan_hash: parsePlanHash(out.plan_hash) };
2394
+ }
2395
+ /**
2396
+ * Starts a workflow run using a precompiled plan hash.
2397
+ *
2398
+ * Use workflows.compile() to compile a workflow spec and obtain a plan_hash,
2399
+ * then use this method to start runs without re-compiling each time.
2400
+ * This is useful for workflows that are run repeatedly with the same structure
2401
+ * but different inputs.
2402
+ *
2403
+ * The plan_hash must have been compiled in the current server session;
2404
+ * if the server has restarted since compilation, the plan will not be found
2405
+ * and you'll need to recompile.
2406
+ */
2407
+ async createFromPlan(planHash, options = {}) {
2408
+ const metrics = mergeMetrics(this.metrics, options.metrics);
2409
+ const trace = mergeTrace(this.trace, options.trace);
2410
+ const authHeaders = await this.auth.authForResponses();
2411
+ const headers = { ...options.headers || {} };
2412
+ this.applyCustomerHeader(headers, options.customerId);
2413
+ const payload = { plan_hash: planHash };
2414
+ if (options.sessionId?.trim()) {
2415
+ payload.session_id = options.sessionId.trim();
2416
+ }
2417
+ if (options.idempotencyKey?.trim()) {
2418
+ payload.options = { idempotency_key: options.idempotencyKey.trim() };
2419
+ }
2420
+ if (options.input) {
2421
+ payload.input = options.input;
2422
+ }
2423
+ if (options.modelOverride?.trim()) {
2424
+ payload.model_override = options.modelOverride.trim();
2425
+ }
2426
+ if (options.modelOverrides) {
2427
+ const nodes = options.modelOverrides.nodes;
2428
+ const fanoutSubnodes = options.modelOverrides.fanoutSubnodes;
2429
+ if (nodes && Object.keys(nodes).length > 0 || fanoutSubnodes && fanoutSubnodes.length > 0) {
2430
+ payload.model_overrides = {
2431
+ nodes,
2432
+ fanout_subnodes: fanoutSubnodes?.map((entry) => ({
2433
+ parent_id: entry.parentId,
2434
+ subnode_id: entry.subnodeId,
2435
+ model: entry.model
2436
+ }))
2437
+ };
2438
+ }
2439
+ }
2440
+ if (options.stream !== void 0) {
2441
+ payload.stream = options.stream;
2442
+ }
2184
2443
  const out = await this.http.json(RUNS_PATH, {
2185
2444
  method: "POST",
2186
2445
  headers,
@@ -2523,290 +2782,460 @@ var ImagesClient = class {
2523
2782
  }
2524
2783
  };
2525
2784
 
2526
- // src/sessions/context_management.ts
2527
- var DEFAULT_CONTEXT_BUFFER_TOKENS = 256;
2528
- var CONTEXT_BUFFER_RATIO = 0.02;
2529
- var MESSAGE_OVERHEAD_TOKENS = 6;
2530
- var TOOL_CALL_OVERHEAD_TOKENS = 4;
2531
- var CHARS_PER_TOKEN = 4;
2532
- var IMAGE_TOKENS_LOW_DETAIL = 85;
2533
- var IMAGE_TOKENS_HIGH_DETAIL = 1e3;
2534
- var modelContextCache = /* @__PURE__ */ new WeakMap();
2535
- function createModelContextResolver(client) {
2536
- return async (modelId) => {
2537
- const entry = getModelContextCacheEntry(client);
2538
- const key = String(modelId);
2539
- const cached = entry.byId.get(key);
2540
- if (cached !== void 0) {
2541
- return cached;
2542
- }
2543
- await populateModelContextCache(client, entry);
2544
- const resolved = entry.byId.get(key);
2545
- if (resolved === void 0) {
2546
- throw new ConfigError(
2547
- `Unknown model "${key}"; ensure the model exists in the ModelRelay catalog`
2548
- );
2785
+ // src/state_handles.ts
2786
+ var MAX_STATE_HANDLE_TTL_SECONDS = 31536e3;
2787
+ var STATE_HANDLES_PATH = "/state-handles";
2788
+ var StateHandlesClient = class {
2789
+ constructor(http, auth) {
2790
+ this.http = http;
2791
+ this.auth = auth;
2792
+ }
2793
+ /** Make an authenticated request to the state handles API. */
2794
+ async request(method, path, body) {
2795
+ const auth = await this.auth.authForResponses();
2796
+ return this.http.json(path, {
2797
+ method,
2798
+ body,
2799
+ apiKey: auth.apiKey,
2800
+ accessToken: auth.accessToken
2801
+ });
2802
+ }
2803
+ async create(request = {}) {
2804
+ if (request.ttl_seconds !== void 0) {
2805
+ if (request.ttl_seconds <= 0) {
2806
+ throw new Error("ttl_seconds must be positive");
2807
+ }
2808
+ if (request.ttl_seconds > MAX_STATE_HANDLE_TTL_SECONDS) {
2809
+ throw new Error("ttl_seconds exceeds maximum (1 year)");
2810
+ }
2549
2811
  }
2550
- return resolved;
2551
- };
2552
- }
2553
- async function buildSessionInputWithContext(messages, options, defaultModel, resolveModelContext) {
2554
- const strategy = options.contextManagement ?? "none";
2555
- if (strategy === "none") {
2556
- return messagesToInput(messages);
2812
+ return this.request("POST", STATE_HANDLES_PATH, request);
2557
2813
  }
2558
- if (strategy === "summarize") {
2559
- throw new ConfigError("contextManagement 'summarize' is not implemented yet");
2814
+ async list(params = {}) {
2815
+ const { limit, offset } = params;
2816
+ if (limit !== void 0 && (limit <= 0 || limit > 100)) {
2817
+ throw new Error("limit must be between 1 and 100");
2818
+ }
2819
+ if (offset !== void 0 && offset < 0) {
2820
+ throw new Error("offset must be non-negative");
2821
+ }
2822
+ const query = new URLSearchParams();
2823
+ if (limit !== void 0) {
2824
+ query.set("limit", String(limit));
2825
+ }
2826
+ if (offset !== void 0 && offset > 0) {
2827
+ query.set("offset", String(offset));
2828
+ }
2829
+ const path = query.toString() ? `${STATE_HANDLES_PATH}?${query.toString()}` : STATE_HANDLES_PATH;
2830
+ return this.request("GET", path);
2560
2831
  }
2561
- if (strategy !== "truncate") {
2562
- throw new ConfigError(`Unknown contextManagement strategy: ${strategy}`);
2832
+ async delete(stateId) {
2833
+ if (!stateId?.trim()) {
2834
+ throw new Error("state_id is required");
2835
+ }
2836
+ await this.request("DELETE", `${STATE_HANDLES_PATH}/${stateId}`);
2563
2837
  }
2564
- const modelId = options.model ?? defaultModel;
2565
- if (!modelId) {
2566
- throw new ConfigError(
2567
- "model is required for context management; set options.model or a session defaultModel"
2838
+ };
2839
+
2840
+ // src/tool_loop.ts
2841
+ var DEFAULT_MAX_TURNS = 100;
2842
+ async function runToolLoop(config) {
2843
+ const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
2844
+ if (!Number.isFinite(maxTurns) || maxTurns <= 0) {
2845
+ throw new ConfigError("maxTurns must be a positive number");
2846
+ }
2847
+ const tools = config.tools ?? [];
2848
+ const history = config.input.slice();
2849
+ const usage = {
2850
+ inputTokens: 0,
2851
+ outputTokens: 0,
2852
+ totalTokens: 0,
2853
+ llmCalls: 0,
2854
+ toolCalls: 0
2855
+ };
2856
+ for (let turn = 0; turn < maxTurns; turn += 1) {
2857
+ let builder = config.client.new().input(history);
2858
+ if (tools.length > 0) {
2859
+ builder = builder.tools(tools);
2860
+ }
2861
+ if (config.buildRequest) {
2862
+ builder = config.buildRequest(builder);
2863
+ }
2864
+ const response = await config.client.create(
2865
+ builder.build(),
2866
+ config.requestOptions
2568
2867
  );
2868
+ usage.llmCalls += 1;
2869
+ usage.inputTokens += response.usage.inputTokens;
2870
+ usage.outputTokens += response.usage.outputTokens;
2871
+ usage.totalTokens += response.usage.totalTokens;
2872
+ const toolCalls = getAllToolCalls(response);
2873
+ if (toolCalls.length === 0) {
2874
+ const assistantText = getAssistantText(response);
2875
+ if (assistantText) {
2876
+ history.push(createAssistantMessage(assistantText));
2877
+ }
2878
+ return {
2879
+ status: "complete",
2880
+ output: assistantText,
2881
+ response,
2882
+ usage,
2883
+ input: history,
2884
+ turnsUsed: turn + 1
2885
+ };
2886
+ }
2887
+ usage.toolCalls += toolCalls.length;
2888
+ history.push(
2889
+ assistantMessageWithToolCalls(getAssistantText(response), toolCalls)
2890
+ );
2891
+ if (!config.registry) {
2892
+ return {
2893
+ status: "waiting_for_tools",
2894
+ pendingToolCalls: toolCalls,
2895
+ response,
2896
+ usage,
2897
+ input: history,
2898
+ turnsUsed: turn + 1
2899
+ };
2900
+ }
2901
+ const results = await config.registry.executeAll(toolCalls);
2902
+ history.push(...config.registry.resultsToMessages(results));
2569
2903
  }
2570
- const budget = await resolveHistoryBudget(
2571
- modelId,
2572
- options,
2573
- resolveModelContext
2574
- );
2575
- const truncated = truncateMessagesByTokens(
2576
- messages,
2577
- budget.maxHistoryTokens
2578
- );
2579
- if (options.onContextTruncate && truncated.length < messages.length) {
2580
- const info = {
2581
- model: modelId,
2582
- originalMessages: messages.length,
2583
- keptMessages: truncated.length,
2584
- maxHistoryTokens: budget.maxHistoryTokens,
2585
- reservedOutputTokens: budget.reservedOutputTokens
2586
- };
2587
- options.onContextTruncate(info);
2588
- }
2589
- return messagesToInput(truncated);
2904
+ throw new AgentMaxTurnsError(maxTurns);
2590
2905
  }
2591
- function messagesToInput(messages) {
2592
- return messages.map((m) => ({
2593
- type: m.type,
2594
- role: m.role,
2595
- content: m.content,
2596
- toolCalls: m.toolCalls,
2597
- toolCallId: m.toolCallId
2598
- }));
2906
+
2907
+ // src/sessions/types.ts
2908
+ function asSessionId(value) {
2909
+ return value;
2599
2910
  }
2600
- function truncateMessagesByTokens(messages, maxHistoryTokens) {
2601
- const maxTokens = normalizePositiveInt(maxHistoryTokens, "maxHistoryTokens");
2602
- if (messages.length === 0) return [];
2603
- const tokensByIndex = messages.map((msg) => estimateTokensForMessage(msg));
2604
- const systemIndices = messages.map((msg, idx) => msg.role === "system" ? idx : -1).filter((idx) => idx >= 0);
2605
- let selectedSystem = [...systemIndices];
2606
- let systemTokens = sumTokens(tokensByIndex, selectedSystem);
2607
- while (systemTokens > maxTokens && selectedSystem.length > 1) {
2608
- selectedSystem.shift();
2609
- systemTokens = sumTokens(tokensByIndex, selectedSystem);
2911
+ function generateSessionId() {
2912
+ return crypto.randomUUID();
2913
+ }
2914
+
2915
+ // src/sessions/stores/memory_store.ts
2916
+ var MemoryConversationStore = class {
2917
+ constructor() {
2918
+ this.sessions = /* @__PURE__ */ new Map();
2610
2919
  }
2611
- if (systemTokens > maxTokens) {
2612
- throw new ConfigError(
2613
- "maxHistoryTokens is too small to fit the latest system message"
2614
- );
2920
+ async load(id) {
2921
+ const state = this.sessions.get(id);
2922
+ if (!state) return null;
2923
+ return structuredClone(state);
2615
2924
  }
2616
- const selected = new Set(selectedSystem);
2617
- let remaining = maxTokens - systemTokens;
2618
- for (let i = messages.length - 1; i >= 0; i -= 1) {
2619
- if (selected.has(i)) continue;
2620
- const tokens = tokensByIndex[i];
2621
- if (tokens <= remaining) {
2622
- selected.add(i);
2623
- remaining -= tokens;
2624
- }
2925
+ async save(state) {
2926
+ this.sessions.set(state.id, structuredClone(state));
2625
2927
  }
2626
- const result = messages.filter((_, idx) => selected.has(idx));
2627
- if (result.length === 0) {
2628
- throw new ConfigError("No messages fit within maxHistoryTokens");
2928
+ async delete(id) {
2929
+ this.sessions.delete(id);
2629
2930
  }
2630
- return result;
2931
+ async list() {
2932
+ return Array.from(this.sessions.keys());
2933
+ }
2934
+ async close() {
2935
+ this.sessions.clear();
2936
+ }
2937
+ /**
2938
+ * Get the number of sessions in the store.
2939
+ * Useful for testing.
2940
+ */
2941
+ get size() {
2942
+ return this.sessions.size;
2943
+ }
2944
+ };
2945
+ function createMemoryConversationStore() {
2946
+ return new MemoryConversationStore();
2631
2947
  }
2632
- function estimateTokens(text) {
2633
- return Math.ceil(text.length / CHARS_PER_TOKEN);
2948
+
2949
+ // src/sessions/stores/serialization.ts
2950
+ function serializeConversationState(state) {
2951
+ return {
2952
+ ...state,
2953
+ messages: state.messages.map((message) => ({
2954
+ ...message,
2955
+ createdAt: message.createdAt.toISOString()
2956
+ }))
2957
+ };
2634
2958
  }
2635
- function isImagePart(part) {
2636
- if (typeof part !== "object" || part === null) return false;
2637
- const p = part;
2638
- return p.type === "image" || p.type === "image_url";
2959
+ function deserializeConversationState(state) {
2960
+ return {
2961
+ ...state,
2962
+ messages: state.messages.map((message) => ({
2963
+ ...message,
2964
+ createdAt: new Date(message.createdAt)
2965
+ }))
2966
+ };
2639
2967
  }
2640
- function estimateImageTokens(part) {
2641
- const detail = part.detail ?? "auto";
2642
- if (detail === "low") return IMAGE_TOKENS_LOW_DETAIL;
2643
- return IMAGE_TOKENS_HIGH_DETAIL;
2968
+
2969
+ // src/sessions/stores/file_store.ts
2970
+ var DEFAULT_SESSION_DIR = ".modelrelay/sessions";
2971
+ async function loadNodeDeps() {
2972
+ try {
2973
+ const fs = await import("fs/promises");
2974
+ const path = await import("path");
2975
+ const os = await import("os");
2976
+ return { fs, path, os };
2977
+ } catch (err) {
2978
+ throw new ConfigError("file persistence requires a Node.js-compatible runtime");
2979
+ }
2644
2980
  }
2645
- function estimateTokensForMessage(message) {
2646
- const segments = [message.role];
2647
- let imageTokens = 0;
2648
- for (const part of message.content || []) {
2649
- if (part.type === "text" && part.text) {
2650
- segments.push(part.text);
2651
- } else if (isImagePart(part)) {
2652
- imageTokens += estimateImageTokens(part);
2653
- }
2981
+ var FileConversationStore = class {
2982
+ constructor(storagePath) {
2983
+ this.storagePath = storagePath;
2654
2984
  }
2655
- if (message.toolCalls) {
2656
- for (const call of message.toolCalls) {
2657
- if (call.function?.name) segments.push(call.function.name);
2658
- if (call.function?.arguments) segments.push(call.function.arguments);
2985
+ async load(id) {
2986
+ const { fs, path, os } = await loadNodeDeps();
2987
+ const filePath = await this.resolveSessionPath(id, path, os);
2988
+ try {
2989
+ const raw = await fs.readFile(filePath, "utf8");
2990
+ const parsed = JSON.parse(raw);
2991
+ return deserializeConversationState(parsed);
2992
+ } catch (err) {
2993
+ if (isNotFoundError(err)) return null;
2994
+ throw err;
2659
2995
  }
2660
2996
  }
2661
- if (message.toolCallId) {
2662
- segments.push(message.toolCallId);
2997
+ async save(state) {
2998
+ const { fs, path, os } = await loadNodeDeps();
2999
+ const dirPath = await this.resolveSessionDir(path, os);
3000
+ await fs.mkdir(dirPath, { recursive: true, mode: 448 });
3001
+ const filePath = path.join(dirPath, `${state.id}.json`);
3002
+ const payload = JSON.stringify(serializeConversationState(state), null, 2);
3003
+ await fs.writeFile(filePath, payload, { mode: 384 });
2663
3004
  }
2664
- const textTokens = estimateTokens(segments.join("\n"));
2665
- const toolOverhead = message.toolCalls ? message.toolCalls.length * TOOL_CALL_OVERHEAD_TOKENS : 0;
2666
- return textTokens + MESSAGE_OVERHEAD_TOKENS + toolOverhead + imageTokens;
2667
- }
2668
- function normalizePositiveInt(value, label) {
2669
- if (!Number.isFinite(value) || value <= 0) {
2670
- throw new ConfigError(`${label} must be a positive number`);
3005
+ async delete(id) {
3006
+ const { fs, path, os } = await loadNodeDeps();
3007
+ const filePath = await this.resolveSessionPath(id, path, os);
3008
+ try {
3009
+ await fs.unlink(filePath);
3010
+ } catch (err) {
3011
+ if (isNotFoundError(err)) return;
3012
+ throw err;
3013
+ }
2671
3014
  }
2672
- return Math.floor(value);
2673
- }
2674
- function sumTokens(tokensByIndex, indices) {
2675
- return indices.reduce((sum, idx) => sum + tokensByIndex[idx], 0);
2676
- }
2677
- async function resolveHistoryBudget(modelId, options, resolveModelContext) {
2678
- const reservedOutputTokens = options.reserveOutputTokens === void 0 ? void 0 : normalizeNonNegativeInt(
2679
- options.reserveOutputTokens,
2680
- "reserveOutputTokens"
2681
- );
2682
- if (options.maxHistoryTokens !== void 0) {
2683
- return {
2684
- maxHistoryTokens: normalizePositiveInt(
2685
- options.maxHistoryTokens,
2686
- "maxHistoryTokens"
2687
- ),
2688
- reservedOutputTokens
2689
- };
3015
+ async list() {
3016
+ const { fs, path, os } = await loadNodeDeps();
3017
+ const dirPath = await this.resolveSessionDir(path, os);
3018
+ try {
3019
+ const entries = await fs.readdir(dirPath);
3020
+ return entries.filter((entry) => path.extname(entry) === ".json").map((entry) => entry.replace(/\.json$/, ""));
3021
+ } catch (err) {
3022
+ if (isNotFoundError(err)) return [];
3023
+ throw err;
3024
+ }
2690
3025
  }
2691
- const model = await resolveModelContext(modelId);
2692
- if (!model) {
2693
- throw new ConfigError(
2694
- `Unknown model "${modelId}"; ensure the model exists in the ModelRelay catalog`
2695
- );
3026
+ async close() {
2696
3027
  }
2697
- const contextWindow = normalizePositiveInt(model.contextWindow, "context_window");
2698
- const modelOutputTokens = model.maxOutputTokens === void 0 ? 0 : normalizeNonNegativeInt(model.maxOutputTokens, "max_output_tokens");
2699
- const effectiveReserve = reservedOutputTokens ?? modelOutputTokens;
2700
- const buffer = Math.max(
2701
- DEFAULT_CONTEXT_BUFFER_TOKENS,
2702
- Math.ceil(contextWindow * CONTEXT_BUFFER_RATIO)
2703
- );
2704
- const maxHistoryTokens = contextWindow - effectiveReserve - buffer;
2705
- if (maxHistoryTokens <= 0) {
2706
- throw new ConfigError(
2707
- "model context window is too small after reserving output tokens; set maxHistoryTokens explicitly"
2708
- );
3028
+ async resolveSessionPath(id, path, os) {
3029
+ const dirPath = await this.resolveSessionDir(path, os);
3030
+ return path.join(dirPath, `${id}.json`);
2709
3031
  }
2710
- return {
2711
- maxHistoryTokens,
2712
- reservedOutputTokens: effectiveReserve
2713
- };
2714
- }
2715
- function normalizeNonNegativeInt(value, label) {
2716
- if (!Number.isFinite(value) || value < 0) {
2717
- throw new ConfigError(`${label} must be a non-negative number`);
3032
+ async resolveSessionDir(path, os) {
3033
+ if (this.storagePath && this.storagePath.trim()) {
3034
+ return this.storagePath;
3035
+ }
3036
+ return path.join(os.homedir(), DEFAULT_SESSION_DIR);
2718
3037
  }
2719
- return Math.floor(value);
2720
- }
2721
- function getModelContextCacheEntry(client) {
2722
- const existing = modelContextCache.get(client);
2723
- if (existing) return existing;
2724
- const entry = { byId: /* @__PURE__ */ new Map() };
2725
- modelContextCache.set(client, entry);
2726
- return entry;
3038
+ };
3039
+ function createFileConversationStore(storagePath) {
3040
+ return new FileConversationStore(storagePath);
2727
3041
  }
2728
- async function populateModelContextCache(client, entry) {
2729
- if (!entry.listPromise) {
2730
- entry.listPromise = (async () => {
2731
- const response = await client.http.json("/models");
2732
- for (const model of response.models) {
2733
- entry.byId.set(String(model.model_id), {
2734
- contextWindow: model.context_window,
2735
- maxOutputTokens: model.max_output_tokens
2736
- });
2737
- }
2738
- })().finally(() => {
2739
- entry.listPromise = void 0;
2740
- });
2741
- }
2742
- await entry.listPromise;
3042
+ function isNotFoundError(err) {
3043
+ return Boolean(
3044
+ err && typeof err === "object" && "code" in err && err.code === "ENOENT"
3045
+ );
2743
3046
  }
2744
3047
 
2745
- // src/sessions/types.ts
2746
- function asSessionId(value) {
2747
- return value;
3048
+ // src/sessions/stores/sqlite_store.ts
3049
+ var DEFAULT_DB_PATH = ".modelrelay/sessions.sqlite";
3050
+ async function loadNodeDeps2() {
3051
+ try {
3052
+ const path = await import("path");
3053
+ const os = await import("os");
3054
+ return { path, os };
3055
+ } catch (err) {
3056
+ throw new ConfigError("sqlite persistence requires a Node.js-compatible runtime");
3057
+ }
2748
3058
  }
2749
- function generateSessionId() {
2750
- return crypto.randomUUID();
3059
+ async function loadSqlite() {
3060
+ try {
3061
+ const mod = await import("better-sqlite3");
3062
+ const Database = mod.default ?? mod;
3063
+ if (typeof Database !== "function") {
3064
+ throw new Error("better-sqlite3 export missing");
3065
+ }
3066
+ return Database;
3067
+ } catch (err) {
3068
+ throw new ConfigError(
3069
+ "sqlite persistence requires the optional 'better-sqlite3' dependency"
3070
+ );
3071
+ }
2751
3072
  }
2752
-
2753
- // src/sessions/stores/memory_store.ts
2754
- var MemorySessionStore = class {
2755
- constructor() {
2756
- this.sessions = /* @__PURE__ */ new Map();
3073
+ var SqliteConversationStore = class {
3074
+ constructor(storagePath) {
3075
+ this.storagePath = storagePath;
2757
3076
  }
2758
3077
  async load(id) {
2759
- const state = this.sessions.get(id);
2760
- if (!state) return null;
2761
- return structuredClone(state);
3078
+ const statements = await this.getStatements();
3079
+ const row = statements.get.get({ id });
3080
+ if (!row) return null;
3081
+ const parsed = {
3082
+ id: row.id,
3083
+ messages: JSON.parse(row.messages),
3084
+ artifacts: JSON.parse(row.artifacts),
3085
+ metadata: JSON.parse(row.metadata),
3086
+ createdAt: row.createdAt,
3087
+ updatedAt: row.updatedAt
3088
+ };
3089
+ return deserializeConversationState(parsed);
2762
3090
  }
2763
3091
  async save(state) {
2764
- this.sessions.set(state.id, structuredClone(state));
3092
+ const statements = await this.getStatements();
3093
+ const payload = serializeConversationState(state);
3094
+ statements.save.run({
3095
+ id: payload.id,
3096
+ messages: JSON.stringify(payload.messages),
3097
+ artifacts: JSON.stringify(payload.artifacts ?? {}),
3098
+ metadata: JSON.stringify(payload.metadata ?? {}),
3099
+ created_at: payload.createdAt,
3100
+ updated_at: payload.updatedAt
3101
+ });
2765
3102
  }
2766
3103
  async delete(id) {
2767
- this.sessions.delete(id);
3104
+ const statements = await this.getStatements();
3105
+ statements.delete.run({ id });
2768
3106
  }
2769
3107
  async list() {
2770
- return Array.from(this.sessions.keys());
3108
+ const statements = await this.getStatements();
3109
+ const rows = statements.list.all();
3110
+ return rows.map((row) => row.id);
2771
3111
  }
2772
3112
  async close() {
2773
- this.sessions.clear();
3113
+ if (this.db) {
3114
+ this.db.close();
3115
+ this.db = void 0;
3116
+ this.statements = void 0;
3117
+ this.initPromise = void 0;
3118
+ }
3119
+ }
3120
+ async ensureInitialized() {
3121
+ if (this.db) return;
3122
+ if (!this.initPromise) {
3123
+ this.initPromise = this.initialize();
3124
+ }
3125
+ await this.initPromise;
3126
+ }
3127
+ async getStatements() {
3128
+ await this.ensureInitialized();
3129
+ if (!this.statements) {
3130
+ throw new Error("Database initialization failed");
3131
+ }
3132
+ return this.statements;
3133
+ }
3134
+ async initialize() {
3135
+ const { path, os } = await loadNodeDeps2();
3136
+ const Database = await loadSqlite();
3137
+ const dbPath = this.resolveDbPath(path, os);
3138
+ this.db = new Database(dbPath);
3139
+ this.db.exec(`
3140
+ CREATE TABLE IF NOT EXISTS conversations (
3141
+ id TEXT PRIMARY KEY,
3142
+ messages TEXT NOT NULL,
3143
+ artifacts TEXT NOT NULL,
3144
+ metadata TEXT NOT NULL,
3145
+ created_at TEXT NOT NULL,
3146
+ updated_at TEXT NOT NULL
3147
+ )
3148
+ `);
3149
+ this.statements = {
3150
+ get: this.db.prepare(
3151
+ "SELECT id, messages, artifacts, metadata, created_at as createdAt, updated_at as updatedAt FROM conversations WHERE id = @id"
3152
+ ),
3153
+ save: this.db.prepare(
3154
+ "INSERT INTO conversations (id, messages, artifacts, metadata, created_at, updated_at) VALUES (@id, @messages, @artifacts, @metadata, @created_at, @updated_at) ON CONFLICT(id) DO UPDATE SET messages = excluded.messages, artifacts = excluded.artifacts, metadata = excluded.metadata, updated_at = excluded.updated_at"
3155
+ ),
3156
+ delete: this.db.prepare("DELETE FROM conversations WHERE id = @id"),
3157
+ list: this.db.prepare("SELECT id FROM conversations ORDER BY id")
3158
+ };
2774
3159
  }
2775
- /**
2776
- * Get the number of sessions in the store.
2777
- * Useful for testing.
2778
- */
2779
- get size() {
2780
- return this.sessions.size;
3160
+ resolveDbPath(path, os) {
3161
+ if (this.storagePath && this.storagePath.trim()) {
3162
+ return this.storagePath;
3163
+ }
3164
+ return path.join(os.homedir(), DEFAULT_DB_PATH);
2781
3165
  }
2782
3166
  };
2783
- function createMemorySessionStore() {
2784
- return new MemorySessionStore();
3167
+ function createSqliteConversationStore(storagePath) {
3168
+ return new SqliteConversationStore(storagePath);
3169
+ }
3170
+
3171
+ // src/sessions/utils.ts
3172
+ function messagesToInput(messages) {
3173
+ return messages.map((m) => ({
3174
+ type: m.type,
3175
+ role: m.role,
3176
+ content: m.content,
3177
+ toolCalls: m.toolCalls,
3178
+ toolCallId: m.toolCallId
3179
+ }));
3180
+ }
3181
+ function mergeTools(defaults, overrides) {
3182
+ if (!defaults && !overrides) return void 0;
3183
+ if (!defaults) return overrides;
3184
+ if (!overrides) return defaults;
3185
+ const merged = /* @__PURE__ */ new Map();
3186
+ for (const tool of defaults) {
3187
+ if (tool.type === "function" && tool.function) {
3188
+ merged.set(tool.function.name, tool);
3189
+ }
3190
+ }
3191
+ for (const tool of overrides) {
3192
+ if (tool.type === "function" && tool.function) {
3193
+ merged.set(tool.function.name, tool);
3194
+ }
3195
+ }
3196
+ return Array.from(merged.values());
3197
+ }
3198
+ function emptyUsage() {
3199
+ return {
3200
+ inputTokens: 0,
3201
+ outputTokens: 0,
3202
+ totalTokens: 0,
3203
+ llmCalls: 0,
3204
+ toolCalls: 0
3205
+ };
3206
+ }
3207
+ function createRequestBuilder(config) {
3208
+ return (builder) => {
3209
+ let next = builder;
3210
+ if (config.model) {
3211
+ next = next.model(config.model);
3212
+ }
3213
+ if (config.provider) {
3214
+ next = next.provider(config.provider);
3215
+ }
3216
+ if (config.customerId) {
3217
+ next = next.customerId(config.customerId);
3218
+ }
3219
+ return next;
3220
+ };
2785
3221
  }
2786
3222
 
2787
3223
  // src/sessions/local_session.ts
3224
+ var DEFAULT_MAX_TURNS2 = 100;
2788
3225
  var LocalSession = class _LocalSession {
2789
3226
  constructor(client, store, options, existingState) {
2790
3227
  this.type = "local";
2791
3228
  this.messages = [];
2792
3229
  this.artifacts = /* @__PURE__ */ new Map();
2793
- this.nextSeq = 1;
2794
- this.currentEvents = [];
2795
- this.currentUsage = {
2796
- inputTokens: 0,
2797
- outputTokens: 0,
2798
- totalTokens: 0,
2799
- llmCalls: 0,
2800
- toolCalls: 0
2801
- };
2802
3230
  this.client = client;
2803
3231
  this.store = store;
2804
3232
  this.toolRegistry = options.toolRegistry;
3233
+ this.contextManager = options.contextManager;
2805
3234
  this.defaultModel = options.defaultModel;
2806
3235
  this.defaultProvider = options.defaultProvider;
2807
3236
  this.defaultTools = options.defaultTools;
3237
+ this.systemPrompt = options.systemPrompt;
2808
3238
  this.metadata = options.metadata || {};
2809
- this.resolveModelContext = createModelContextResolver(client);
2810
3239
  if (existingState) {
2811
3240
  this.id = existingState.id;
2812
3241
  this.messages = existingState.messages.map((m) => ({
@@ -2814,7 +3243,6 @@ var LocalSession = class _LocalSession {
2814
3243
  createdAt: new Date(m.createdAt)
2815
3244
  }));
2816
3245
  this.artifacts = new Map(Object.entries(existingState.artifacts));
2817
- this.nextSeq = this.messages.length + 1;
2818
3246
  this.createdAt = new Date(existingState.createdAt);
2819
3247
  this.updatedAt = new Date(existingState.updatedAt);
2820
3248
  } else {
@@ -2831,7 +3259,11 @@ var LocalSession = class _LocalSession {
2831
3259
  * @returns A new LocalSession instance
2832
3260
  */
2833
3261
  static create(client, options = {}) {
2834
- const store = createStore(options.persistence || "memory", options.storagePath);
3262
+ const store = createStore(
3263
+ options.conversationStore,
3264
+ options.persistence || "memory",
3265
+ options.storagePath
3266
+ );
2835
3267
  return new _LocalSession(client, store, options);
2836
3268
  }
2837
3269
  /**
@@ -2844,7 +3276,11 @@ var LocalSession = class _LocalSession {
2844
3276
  */
2845
3277
  static async resume(client, sessionId, options = {}) {
2846
3278
  const id = typeof sessionId === "string" ? asSessionId(sessionId) : sessionId;
2847
- const store = createStore(options.persistence || "memory", options.storagePath);
3279
+ const store = createStore(
3280
+ options.conversationStore,
3281
+ options.persistence || "memory",
3282
+ options.storagePath
3283
+ );
2848
3284
  const state = await store.load(id);
2849
3285
  if (!state) {
2850
3286
  await store.close();
@@ -2856,106 +3292,147 @@ var LocalSession = class _LocalSession {
2856
3292
  return this.messages;
2857
3293
  }
2858
3294
  async run(prompt, options = {}) {
2859
- const userMessage = this.addMessage({
2860
- type: "message",
2861
- role: "user",
2862
- content: [{ type: "text", text: prompt }]
2863
- });
2864
- this.currentEvents = [];
2865
- this.currentUsage = {
2866
- inputTokens: 0,
2867
- outputTokens: 0,
2868
- totalTokens: 0,
2869
- llmCalls: 0,
2870
- toolCalls: 0
2871
- };
2872
- this.currentRunId = void 0;
2873
- this.currentNodeId = void 0;
2874
- this.currentWaiting = void 0;
3295
+ this.pendingLoop = void 0;
3296
+ this.messages.push(buildMessage(createUserMessage(prompt), this.messages.length + 1));
3297
+ this.updatedAt = /* @__PURE__ */ new Date();
3298
+ const baseInput = messagesToInput(this.messages);
3299
+ const contextOptions = this.buildContextOptions(options);
2875
3300
  try {
2876
- const input = await this.buildInput(options);
3301
+ const prepared = await this.prepareInput(baseInput, contextOptions);
2877
3302
  const tools = mergeTools(this.defaultTools, options.tools);
2878
- const spec = {
2879
- kind: "workflow",
2880
- name: `session-${this.id}-turn-${this.nextSeq}`,
2881
- model: options.model || this.defaultModel,
2882
- nodes: [
2883
- {
2884
- id: "main",
2885
- type: "llm",
2886
- input,
3303
+ const modelId = options.model ?? this.defaultModel;
3304
+ const providerId = options.provider ?? this.defaultProvider;
3305
+ const requestOptions = options.signal ? { signal: options.signal } : {};
3306
+ const outcome = await runToolLoop({
3307
+ client: this.client.responses,
3308
+ input: prepared,
3309
+ tools,
3310
+ registry: this.toolRegistry,
3311
+ maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS2,
3312
+ requestOptions,
3313
+ buildRequest: createRequestBuilder({
3314
+ model: modelId,
3315
+ provider: providerId,
3316
+ customerId: options.customerId
3317
+ })
3318
+ });
3319
+ const cleanInput = stripSystemPrompt(outcome.input, this.systemPrompt);
3320
+ this.replaceHistory(cleanInput);
3321
+ await this.persist();
3322
+ const usage = outcome.usage;
3323
+ if (outcome.status === "waiting_for_tools") {
3324
+ const pendingRequestOptions = { ...requestOptions };
3325
+ delete pendingRequestOptions.signal;
3326
+ this.pendingLoop = {
3327
+ input: cleanInput,
3328
+ usage,
3329
+ remainingTurns: remainingTurns(
3330
+ options.maxTurns ?? DEFAULT_MAX_TURNS2,
3331
+ outcome.turnsUsed
3332
+ ),
3333
+ config: {
3334
+ model: modelId,
3335
+ provider: providerId,
2887
3336
  tools,
2888
- tool_execution: this.toolRegistry ? { mode: "client" } : void 0
3337
+ customerId: options.customerId,
3338
+ requestOptions: pendingRequestOptions,
3339
+ contextOptions
2889
3340
  }
2890
- ],
2891
- outputs: [{ name: "result", from: "main" }]
3341
+ };
3342
+ return {
3343
+ status: "waiting_for_tools",
3344
+ pendingTools: mapPendingToolCalls(outcome.pendingToolCalls),
3345
+ response: outcome.response,
3346
+ usage
3347
+ };
3348
+ }
3349
+ return {
3350
+ status: "complete",
3351
+ output: outcome.output,
3352
+ response: outcome.response,
3353
+ usage
2892
3354
  };
2893
- const run = await this.client.runs.create(spec, {
2894
- customerId: options.customerId
2895
- });
2896
- this.currentRunId = run.run_id;
2897
- return await this.processRunEvents(options.signal);
2898
3355
  } catch (err) {
2899
3356
  const error = err instanceof Error ? err : new Error(String(err));
2900
3357
  return {
2901
3358
  status: "error",
2902
3359
  error: error.message,
2903
- runId: this.currentRunId || parseRunId("unknown"),
2904
- usage: this.currentUsage,
2905
- events: this.currentEvents
3360
+ cause: error,
3361
+ usage: emptyUsage()
2906
3362
  };
2907
3363
  }
2908
3364
  }
2909
3365
  async submitToolResults(results) {
2910
- if (!this.currentRunId || !this.currentNodeId || !this.currentWaiting) {
3366
+ if (!this.pendingLoop) {
2911
3367
  throw new Error("No pending tool calls to submit results for");
2912
3368
  }
2913
- await this.client.runs.submitToolResults(this.currentRunId, {
2914
- node_id: this.currentNodeId,
2915
- step: this.currentWaiting.step,
2916
- request_id: this.currentWaiting.request_id,
2917
- results: results.map((r) => ({
2918
- tool_call: {
2919
- id: r.toolCallId,
2920
- name: r.toolName
2921
- },
2922
- output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
2923
- }))
3369
+ const pending = this.pendingLoop;
3370
+ this.pendingLoop = void 0;
3371
+ if (pending.remainingTurns <= 0) {
3372
+ throw new AgentMaxTurnsError(0);
3373
+ }
3374
+ const resultItems = results.map((result) => {
3375
+ const content = result.error ? `Error: ${result.error}` : typeof result.result === "string" ? result.result : JSON.stringify(result.result);
3376
+ return toolResultMessage(result.toolCallId, content);
2924
3377
  });
2925
- this.currentWaiting = void 0;
2926
- return await this.processRunEvents();
2927
- }
2928
- getArtifacts() {
2929
- return new Map(this.artifacts);
2930
- }
3378
+ const baseInput = [...pending.input, ...resultItems];
3379
+ try {
3380
+ const prepared = await this.prepareInput(baseInput, pending.config.contextOptions);
3381
+ const outcome = await runToolLoop({
3382
+ client: this.client.responses,
3383
+ input: prepared,
3384
+ tools: pending.config.tools,
3385
+ registry: this.toolRegistry,
3386
+ maxTurns: pending.remainingTurns,
3387
+ requestOptions: pending.config.requestOptions,
3388
+ buildRequest: createRequestBuilder(pending.config)
3389
+ });
3390
+ const cleanInput = stripSystemPrompt(outcome.input, this.systemPrompt);
3391
+ this.replaceHistory(cleanInput);
3392
+ await this.persist();
3393
+ const usage = mergeUsage(pending.usage, outcome.usage);
3394
+ if (outcome.status === "waiting_for_tools") {
3395
+ this.pendingLoop = {
3396
+ input: cleanInput,
3397
+ usage,
3398
+ remainingTurns: remainingTurns(
3399
+ pending.remainingTurns,
3400
+ outcome.turnsUsed
3401
+ ),
3402
+ config: pending.config
3403
+ };
3404
+ return {
3405
+ status: "waiting_for_tools",
3406
+ pendingTools: mapPendingToolCalls(outcome.pendingToolCalls),
3407
+ response: outcome.response,
3408
+ usage
3409
+ };
3410
+ }
3411
+ return {
3412
+ status: "complete",
3413
+ output: outcome.output,
3414
+ response: outcome.response,
3415
+ usage
3416
+ };
3417
+ } catch (err) {
3418
+ const error = err instanceof Error ? err : new Error(String(err));
3419
+ return {
3420
+ status: "error",
3421
+ error: error.message,
3422
+ cause: error,
3423
+ usage: pending.usage
3424
+ };
3425
+ }
3426
+ }
3427
+ getArtifacts() {
3428
+ return new Map(this.artifacts);
3429
+ }
2931
3430
  async close() {
2932
3431
  await this.persist();
2933
3432
  await this.store.close();
2934
3433
  }
2935
3434
  /**
2936
3435
  * Sync this local session's messages to a remote session.
2937
- *
2938
- * This uploads all local messages to the remote session, enabling
2939
- * cross-device access and server-side backup. Messages are synced
2940
- * in order and the remote session's history will contain all local
2941
- * messages after sync completes.
2942
- *
2943
- * @param remoteSession - The remote session to sync to
2944
- * @param options - Optional sync configuration
2945
- * @returns Sync result with message count
2946
- *
2947
- * @example
2948
- * ```typescript
2949
- * // Create local session and work offline
2950
- * const local = LocalSession.create(client, { ... });
2951
- * await local.run("Implement the feature");
2952
- *
2953
- * // Later, sync to remote for backup/sharing
2954
- * const remote = await RemoteSession.create(client);
2955
- * const result = await local.syncTo(remote, {
2956
- * onProgress: (synced, total) => console.log(`${synced}/${total}`),
2957
- * });
2958
- * ```
2959
3436
  */
2960
3437
  async syncTo(remoteSession, options = {}) {
2961
3438
  const { onProgress, signal } = options;
@@ -2997,150 +3474,35 @@ var LocalSession = class _LocalSession {
2997
3474
  // ============================================================================
2998
3475
  // Private Methods
2999
3476
  // ============================================================================
3000
- addMessage(input, runId) {
3001
- const message = {
3002
- ...input,
3003
- seq: this.nextSeq++,
3004
- createdAt: /* @__PURE__ */ new Date(),
3005
- runId
3006
- };
3007
- this.messages.push(message);
3008
- this.updatedAt = /* @__PURE__ */ new Date();
3009
- return message;
3010
- }
3011
- async buildInput(options) {
3012
- return buildSessionInputWithContext(
3013
- this.messages,
3014
- options,
3015
- this.defaultModel,
3016
- this.resolveModelContext
3017
- );
3018
- }
3019
- async processRunEvents(signal) {
3020
- if (!this.currentRunId) {
3021
- throw new Error("No current run");
3022
- }
3023
- const eventStream = await this.client.runs.events(this.currentRunId, {
3024
- afterSeq: this.currentEvents.length
3025
- });
3026
- for await (const event of eventStream) {
3027
- if (signal?.aborted) {
3028
- return {
3029
- status: "canceled",
3030
- runId: this.currentRunId,
3031
- usage: this.currentUsage,
3032
- events: this.currentEvents
3033
- };
3034
- }
3035
- this.currentEvents.push(event);
3036
- switch (event.type) {
3037
- case "node_llm_call":
3038
- this.currentUsage = {
3039
- ...this.currentUsage,
3040
- llmCalls: this.currentUsage.llmCalls + 1,
3041
- inputTokens: this.currentUsage.inputTokens + (event.llm_call.usage?.input_tokens || 0),
3042
- outputTokens: this.currentUsage.outputTokens + (event.llm_call.usage?.output_tokens || 0),
3043
- totalTokens: this.currentUsage.totalTokens + (event.llm_call.usage?.total_tokens || 0)
3044
- };
3045
- break;
3046
- case "node_tool_call":
3047
- this.currentUsage = {
3048
- ...this.currentUsage,
3049
- toolCalls: this.currentUsage.toolCalls + 1
3050
- };
3051
- break;
3052
- case "node_waiting":
3053
- this.currentNodeId = event.node_id;
3054
- this.currentWaiting = event.waiting;
3055
- if (this.toolRegistry) {
3056
- const results = await this.executeTools(event.waiting.pending_tool_calls);
3057
- return await this.submitToolResults(results);
3058
- }
3059
- return {
3060
- status: "waiting_for_tools",
3061
- pendingTools: event.waiting.pending_tool_calls.map((tc) => ({
3062
- toolCallId: tc.tool_call.id,
3063
- name: tc.tool_call.name,
3064
- arguments: tc.tool_call.arguments
3065
- })),
3066
- runId: this.currentRunId,
3067
- usage: this.currentUsage,
3068
- events: this.currentEvents
3069
- };
3070
- case "run_completed":
3071
- const runState = await this.client.runs.get(this.currentRunId);
3072
- const output = extractTextOutput(runState.outputs || {});
3073
- if (output) {
3074
- this.addMessage(
3075
- {
3076
- type: "message",
3077
- role: "assistant",
3078
- content: [{ type: "text", text: output }]
3079
- },
3080
- this.currentRunId
3081
- );
3082
- }
3083
- await this.persist();
3084
- return {
3085
- status: "complete",
3086
- output,
3087
- runId: this.currentRunId,
3088
- usage: this.currentUsage,
3089
- events: this.currentEvents
3090
- };
3091
- case "run_failed":
3092
- return {
3093
- status: "error",
3094
- error: event.error.message,
3095
- runId: this.currentRunId,
3096
- usage: this.currentUsage,
3097
- events: this.currentEvents
3098
- };
3099
- case "run_canceled":
3100
- return {
3101
- status: "canceled",
3102
- error: event.error.message,
3103
- runId: this.currentRunId,
3104
- usage: this.currentUsage,
3105
- events: this.currentEvents
3106
- };
3107
- }
3108
- }
3477
+ buildContextOptions(options) {
3478
+ if (!this.contextManager) return null;
3479
+ if (options.contextManagement === "none") return null;
3109
3480
  return {
3110
- status: "error",
3111
- error: "Run event stream ended unexpectedly",
3112
- runId: this.currentRunId,
3113
- usage: this.currentUsage,
3114
- events: this.currentEvents
3481
+ model: options.model ?? this.defaultModel,
3482
+ strategy: options.contextManagement,
3483
+ maxHistoryTokens: options.maxHistoryTokens,
3484
+ reserveOutputTokens: options.reserveOutputTokens,
3485
+ onTruncate: options.onContextTruncate
3115
3486
  };
3116
3487
  }
3117
- async executeTools(pendingTools) {
3118
- if (!this.toolRegistry) {
3119
- throw new Error("No tool registry configured");
3488
+ async prepareInput(input, contextOptions) {
3489
+ let prepared = input;
3490
+ if (this.systemPrompt) {
3491
+ prepared = [createSystemMessage(this.systemPrompt), ...prepared];
3120
3492
  }
3121
- const results = [];
3122
- for (const pending of pendingTools) {
3123
- try {
3124
- const result = await this.toolRegistry.execute({
3125
- id: pending.tool_call.id,
3126
- type: "function",
3127
- function: {
3128
- name: pending.tool_call.name,
3129
- arguments: pending.tool_call.arguments
3130
- }
3131
- });
3132
- results.push(result);
3133
- } catch (err) {
3134
- const error = err instanceof Error ? err : new Error(String(err));
3135
- results.push({
3136
- toolCallId: pending.tool_call.id,
3137
- toolName: pending.tool_call.name,
3138
- result: null,
3139
- error: error.message
3140
- });
3141
- }
3493
+ if (!this.contextManager || !contextOptions) {
3494
+ return prepared;
3142
3495
  }
3143
- return results;
3496
+ return this.contextManager.prepare(prepared, contextOptions);
3497
+ }
3498
+ replaceHistory(input) {
3499
+ const now = /* @__PURE__ */ new Date();
3500
+ this.messages = input.map((item, idx) => ({
3501
+ ...item,
3502
+ seq: idx + 1,
3503
+ createdAt: now
3504
+ }));
3505
+ this.updatedAt = now;
3144
3506
  }
3145
3507
  async persist() {
3146
3508
  const state = {
@@ -3157,159 +3519,373 @@ var LocalSession = class _LocalSession {
3157
3519
  await this.store.save(state);
3158
3520
  }
3159
3521
  };
3160
- function createStore(persistence, storagePath) {
3522
+ function createStore(custom, persistence, storagePath) {
3523
+ if (custom) {
3524
+ return custom;
3525
+ }
3161
3526
  switch (persistence) {
3162
3527
  case "memory":
3163
- return createMemorySessionStore();
3528
+ return createMemoryConversationStore();
3164
3529
  case "file":
3165
- throw new Error("File persistence not yet implemented");
3530
+ return createFileConversationStore(storagePath);
3166
3531
  case "sqlite":
3167
- throw new Error("SQLite persistence not yet implemented");
3532
+ return createSqliteConversationStore(storagePath);
3168
3533
  default:
3169
3534
  throw new Error(`Unknown persistence mode: ${persistence}`);
3170
3535
  }
3171
3536
  }
3172
- function mergeTools(defaults, overrides) {
3173
- if (!defaults && !overrides) return void 0;
3174
- if (!defaults) return overrides;
3175
- if (!overrides) return defaults;
3176
- const merged = /* @__PURE__ */ new Map();
3177
- for (const tool of defaults) {
3178
- if (tool.type === "function" && tool.function) {
3179
- merged.set(tool.function.name, tool);
3180
- }
3537
+ function buildMessage(item, seq) {
3538
+ return {
3539
+ ...item,
3540
+ seq,
3541
+ createdAt: /* @__PURE__ */ new Date()
3542
+ };
3543
+ }
3544
+ function stripSystemPrompt(input, systemPrompt) {
3545
+ if (!systemPrompt || input.length === 0) {
3546
+ return input;
3181
3547
  }
3182
- for (const tool of overrides) {
3183
- if (tool.type === "function" && tool.function) {
3184
- merged.set(tool.function.name, tool);
3185
- }
3548
+ const [first, ...rest] = input;
3549
+ if (first.role === "system" && first.content?.length === 1 && first.content[0].type === "text" && first.content[0].text === systemPrompt) {
3550
+ return rest;
3186
3551
  }
3187
- return Array.from(merged.values());
3552
+ return input;
3188
3553
  }
3189
- function isOutputMessage(item) {
3190
- return typeof item === "object" && item !== null && "type" in item && typeof item.type === "string";
3191
- }
3192
- function isContentPiece(c) {
3193
- return typeof c === "object" && c !== null && "type" in c && typeof c.type === "string";
3194
- }
3195
- function hasOutputArray(obj) {
3196
- return "output" in obj && Array.isArray(obj.output);
3197
- }
3198
- function hasContentArray(obj) {
3199
- return "content" in obj && Array.isArray(obj.content);
3200
- }
3201
- function extractTextOutput(outputs) {
3202
- const result = outputs.result;
3203
- if (typeof result === "string") return result;
3204
- if (result && typeof result === "object") {
3205
- if (hasOutputArray(result)) {
3206
- const textParts = result.output.filter(
3207
- (item) => isOutputMessage(item) && item.type === "message" && item.role === "assistant"
3208
- ).flatMap(
3209
- (item) => (item.content || []).filter((c) => isContentPiece(c) && c.type === "text").map((c) => c.text ?? "")
3210
- ).filter((text) => text.length > 0);
3211
- if (textParts.length > 0) {
3212
- return textParts.join("\n");
3213
- }
3214
- }
3215
- if (hasContentArray(result)) {
3216
- const textParts = result.content.filter((c) => isContentPiece(c) && c.type === "text").map((c) => c.text ?? "").filter((text) => text.length > 0);
3217
- if (textParts.length > 0) {
3218
- return textParts.join("\n");
3219
- }
3554
+ function mapPendingToolCalls(calls) {
3555
+ return calls.map((call) => {
3556
+ if (!call.function?.name) {
3557
+ throw new Error(`Tool call ${call.id} missing function name`);
3220
3558
  }
3559
+ return {
3560
+ toolCallId: call.id,
3561
+ name: call.function.name,
3562
+ arguments: call.function.arguments ?? "{}"
3563
+ };
3564
+ });
3565
+ }
3566
+ function remainingTurns(maxTurns, turnsUsed) {
3567
+ if (maxTurns === Number.MAX_SAFE_INTEGER) {
3568
+ return maxTurns;
3221
3569
  }
3222
- return void 0;
3570
+ return Math.max(0, maxTurns - turnsUsed);
3571
+ }
3572
+ function mergeUsage(base, add) {
3573
+ return {
3574
+ inputTokens: base.inputTokens + add.inputTokens,
3575
+ outputTokens: base.outputTokens + add.outputTokens,
3576
+ totalTokens: base.totalTokens + add.totalTokens,
3577
+ llmCalls: base.llmCalls + add.llmCalls,
3578
+ toolCalls: base.toolCalls + add.toolCalls
3579
+ };
3223
3580
  }
3224
3581
  function createLocalSession(client, options = {}) {
3225
3582
  return LocalSession.create(client, options);
3226
3583
  }
3227
3584
 
3228
- // src/sessions/remote_session.ts
3229
- var RemoteSession = class _RemoteSession {
3230
- constructor(client, http, sessionData, options = {}) {
3231
- this.type = "remote";
3232
- this.messages = [];
3233
- this.artifacts = /* @__PURE__ */ new Map();
3234
- this.nextSeq = 1;
3235
- this.pendingMessages = [];
3236
- this.currentEvents = [];
3237
- this.currentUsage = {
3238
- inputTokens: 0,
3239
- outputTokens: 0,
3240
- totalTokens: 0,
3241
- llmCalls: 0,
3242
- toolCalls: 0
3243
- };
3244
- this.client = client;
3245
- this.http = http;
3246
- this.id = asSessionId(sessionData.id);
3247
- this.metadata = sessionData.metadata;
3248
- this.customerId = sessionData.customer_id || options.customerId;
3249
- this.createdAt = new Date(sessionData.created_at);
3250
- this.updatedAt = new Date(sessionData.updated_at);
3251
- this.toolRegistry = options.toolRegistry;
3252
- this.defaultModel = options.defaultModel;
3253
- this.defaultProvider = options.defaultProvider;
3254
- this.defaultTools = options.defaultTools;
3255
- this.resolveModelContext = createModelContextResolver(client);
3256
- if ("messages" in sessionData && sessionData.messages) {
3257
- this.messages = sessionData.messages.map((m) => ({
3258
- type: "message",
3259
- role: m.role,
3260
- content: m.content,
3261
- seq: m.seq,
3262
- createdAt: new Date(m.created_at),
3263
- runId: m.run_id ? parseRunId(m.run_id) : void 0
3264
- }));
3265
- this.nextSeq = this.messages.length + 1;
3585
+ // src/context_manager.ts
3586
+ var DEFAULT_CONTEXT_BUFFER_TOKENS = 256;
3587
+ var CONTEXT_BUFFER_RATIO = 0.02;
3588
+ var MESSAGE_OVERHEAD_TOKENS = 6;
3589
+ var TOOL_CALL_OVERHEAD_TOKENS = 4;
3590
+ var CHARS_PER_TOKEN = 4;
3591
+ var IMAGE_TOKENS_LOW_DETAIL = 85;
3592
+ var IMAGE_TOKENS_HIGH_DETAIL = 1e3;
3593
+ var modelContextCache = /* @__PURE__ */ new WeakMap();
3594
+ function createModelContextResolver(client) {
3595
+ return async (modelId) => {
3596
+ const entry = getModelContextCacheEntry(client);
3597
+ const key = String(modelId);
3598
+ const cached = entry.byId.get(key);
3599
+ if (cached !== void 0) {
3600
+ return cached;
3601
+ }
3602
+ await populateModelContextCache(client, entry);
3603
+ const resolved = entry.byId.get(key);
3604
+ if (resolved === void 0) {
3605
+ throw new ConfigError(
3606
+ `Unknown model "${key}"; ensure the model exists in the ModelRelay catalog`
3607
+ );
3266
3608
  }
3609
+ return resolved;
3610
+ };
3611
+ }
3612
+ var ContextManager = class {
3613
+ constructor(resolveModelContext, defaults = {}) {
3614
+ this.resolveModelContext = resolveModelContext;
3615
+ this.defaults = defaults;
3616
+ }
3617
+ async prepare(input, options = {}) {
3618
+ const merged = {
3619
+ ...this.defaults,
3620
+ ...options
3621
+ };
3622
+ return prepareInputWithContext(input, merged, this.resolveModelContext);
3267
3623
  }
3268
- /**
3269
- * Create a new remote session on the server.
3270
- *
3271
- * @param client - ModelRelay client
3272
- * @param options - Session configuration
3273
- * @returns A new RemoteSession instance
3274
- */
3275
- static async create(client, options = {}) {
3276
- const http = getHTTPClient(client);
3277
- const response = await http.request("/sessions", {
3278
- method: "POST",
3279
- body: {
3280
- customer_id: options.customerId,
3281
- metadata: options.metadata || {}
3282
- }
3283
- });
3284
- const data = await response.json();
3285
- return new _RemoteSession(client, http, data, options);
3624
+ };
3625
+ async function prepareInputWithContext(input, options, resolveModelContext) {
3626
+ const strategy = options.strategy ?? "truncate";
3627
+ if (strategy === "summarize") {
3628
+ throw new ConfigError("context management 'summarize' is not implemented yet");
3286
3629
  }
3287
- /**
3288
- * Get an existing remote session by ID.
3289
- *
3290
- * @param client - ModelRelay client
3291
- * @param sessionId - ID of the session to retrieve
3292
- * @param options - Optional configuration (toolRegistry, defaults)
3293
- * @returns The RemoteSession instance
3294
- */
3295
- static async get(client, sessionId, options = {}) {
3296
- const http = getHTTPClient(client);
3297
- const id = typeof sessionId === "string" ? sessionId : String(sessionId);
3298
- const response = await http.request(`/sessions/${id}`, {
3299
- method: "GET"
3300
- });
3301
- const data = await response.json();
3302
- return new _RemoteSession(client, http, data, options);
3630
+ if (strategy !== "truncate") {
3631
+ throw new ConfigError(`Unknown context management strategy: ${strategy}`);
3303
3632
  }
3304
- /**
3305
- * List remote sessions.
3306
- *
3307
- * @param client - ModelRelay client
3308
- * @param options - List options
3309
- * @returns Paginated list of session info
3310
- */
3311
- static async list(client, options = {}) {
3312
- const http = getHTTPClient(client);
3633
+ const budget = await resolveHistoryBudget(
3634
+ options.model,
3635
+ options,
3636
+ resolveModelContext
3637
+ );
3638
+ const truncated = truncateInputByTokens(input, budget.maxHistoryTokens);
3639
+ if (options.onTruncate && truncated.length < input.length) {
3640
+ if (!options.model) {
3641
+ throw new ConfigError(
3642
+ "model is required for context management; set options.model"
3643
+ );
3644
+ }
3645
+ const info = {
3646
+ model: options.model,
3647
+ originalMessages: input.length,
3648
+ keptMessages: truncated.length,
3649
+ maxHistoryTokens: budget.maxHistoryTokens,
3650
+ reservedOutputTokens: budget.reservedOutputTokens
3651
+ };
3652
+ options.onTruncate(info);
3653
+ }
3654
+ return truncated;
3655
+ }
3656
+ function truncateInputByTokens(input, maxHistoryTokens) {
3657
+ const maxTokens = normalizePositiveInt(maxHistoryTokens, "maxHistoryTokens");
3658
+ if (input.length === 0) return [];
3659
+ const tokensByIndex = input.map((msg) => estimateTokensForMessage(msg));
3660
+ const systemIndices = input.map((msg, idx) => msg.role === "system" ? idx : -1).filter((idx) => idx >= 0);
3661
+ let selectedSystem = [...systemIndices];
3662
+ let systemTokens = sumTokens(tokensByIndex, selectedSystem);
3663
+ while (systemTokens > maxTokens && selectedSystem.length > 1) {
3664
+ selectedSystem.shift();
3665
+ systemTokens = sumTokens(tokensByIndex, selectedSystem);
3666
+ }
3667
+ if (systemTokens > maxTokens) {
3668
+ throw new ConfigError(
3669
+ "maxHistoryTokens is too small to fit the latest system message"
3670
+ );
3671
+ }
3672
+ const selected = new Set(selectedSystem);
3673
+ let remaining = maxTokens - systemTokens;
3674
+ for (let i = input.length - 1; i >= 0; i -= 1) {
3675
+ if (selected.has(i)) continue;
3676
+ const tokens = tokensByIndex[i];
3677
+ if (tokens <= remaining) {
3678
+ selected.add(i);
3679
+ remaining -= tokens;
3680
+ }
3681
+ }
3682
+ const result = input.filter((_, idx) => selected.has(idx));
3683
+ if (result.length === 0) {
3684
+ throw new ConfigError("No messages fit within maxHistoryTokens");
3685
+ }
3686
+ return result;
3687
+ }
3688
+ function estimateTokens(text) {
3689
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
3690
+ }
3691
+ function isImagePart(part) {
3692
+ if (typeof part !== "object" || part === null) return false;
3693
+ const p = part;
3694
+ return p.type === "image" || p.type === "image_url";
3695
+ }
3696
+ function estimateImageTokens(part) {
3697
+ const detail = part.detail ?? "auto";
3698
+ if (detail === "low") return IMAGE_TOKENS_LOW_DETAIL;
3699
+ return IMAGE_TOKENS_HIGH_DETAIL;
3700
+ }
3701
+ function estimateTokensForMessage(message) {
3702
+ const segments = [message.role];
3703
+ let imageTokens = 0;
3704
+ for (const part of message.content || []) {
3705
+ if (part.type === "text" && part.text) {
3706
+ segments.push(part.text);
3707
+ } else if (isImagePart(part)) {
3708
+ imageTokens += estimateImageTokens(part);
3709
+ }
3710
+ }
3711
+ if (message.toolCalls) {
3712
+ for (const call of message.toolCalls) {
3713
+ if (call.function?.name) segments.push(call.function.name);
3714
+ if (call.function?.arguments) segments.push(call.function.arguments);
3715
+ }
3716
+ }
3717
+ if (message.toolCallId) {
3718
+ segments.push(message.toolCallId);
3719
+ }
3720
+ const textTokens = estimateTokens(segments.join("\n"));
3721
+ const toolOverhead = message.toolCalls ? message.toolCalls.length * TOOL_CALL_OVERHEAD_TOKENS : 0;
3722
+ return textTokens + MESSAGE_OVERHEAD_TOKENS + toolOverhead + imageTokens;
3723
+ }
3724
+ function normalizePositiveInt(value, label) {
3725
+ if (!Number.isFinite(value) || value <= 0) {
3726
+ throw new ConfigError(`${label} must be a positive number`);
3727
+ }
3728
+ return Math.floor(value);
3729
+ }
3730
+ function sumTokens(tokensByIndex, indices) {
3731
+ return indices.reduce((sum, idx) => sum + tokensByIndex[idx], 0);
3732
+ }
3733
+ async function resolveHistoryBudget(modelId, options, resolveModelContext) {
3734
+ const reservedOutputTokens = options.reserveOutputTokens === void 0 ? void 0 : normalizeNonNegativeInt(
3735
+ options.reserveOutputTokens,
3736
+ "reserveOutputTokens"
3737
+ );
3738
+ if (options.maxHistoryTokens !== void 0) {
3739
+ return {
3740
+ maxHistoryTokens: normalizePositiveInt(
3741
+ options.maxHistoryTokens,
3742
+ "maxHistoryTokens"
3743
+ ),
3744
+ reservedOutputTokens
3745
+ };
3746
+ }
3747
+ if (!modelId) {
3748
+ throw new ConfigError(
3749
+ "model is required for context management when maxHistoryTokens is not set"
3750
+ );
3751
+ }
3752
+ const model = await resolveModelContext(modelId);
3753
+ if (!model) {
3754
+ throw new ConfigError(
3755
+ `Unknown model "${modelId}"; ensure the model exists in the ModelRelay catalog`
3756
+ );
3757
+ }
3758
+ const contextWindow = normalizePositiveInt(model.contextWindow, "context_window");
3759
+ const modelOutputTokens = model.maxOutputTokens === void 0 ? 0 : normalizeNonNegativeInt(model.maxOutputTokens, "max_output_tokens");
3760
+ const effectiveReserve = reservedOutputTokens ?? modelOutputTokens;
3761
+ const buffer = Math.max(
3762
+ DEFAULT_CONTEXT_BUFFER_TOKENS,
3763
+ Math.ceil(contextWindow * CONTEXT_BUFFER_RATIO)
3764
+ );
3765
+ const maxHistoryTokens = contextWindow - effectiveReserve - buffer;
3766
+ if (maxHistoryTokens <= 0) {
3767
+ throw new ConfigError(
3768
+ "model context window is too small after reserving output tokens; set maxHistoryTokens explicitly"
3769
+ );
3770
+ }
3771
+ return {
3772
+ maxHistoryTokens,
3773
+ reservedOutputTokens: effectiveReserve
3774
+ };
3775
+ }
3776
+ function normalizeNonNegativeInt(value, label) {
3777
+ if (!Number.isFinite(value) || value < 0) {
3778
+ throw new ConfigError(`${label} must be a non-negative number`);
3779
+ }
3780
+ return Math.floor(value);
3781
+ }
3782
+ function getModelContextCacheEntry(client) {
3783
+ const existing = modelContextCache.get(client);
3784
+ if (existing) return existing;
3785
+ const entry = { byId: /* @__PURE__ */ new Map() };
3786
+ modelContextCache.set(client, entry);
3787
+ return entry;
3788
+ }
3789
+ async function populateModelContextCache(client, entry) {
3790
+ if (!entry.listPromise) {
3791
+ entry.listPromise = (async () => {
3792
+ const response = await client.http.json("/models");
3793
+ for (const model of response.models) {
3794
+ entry.byId.set(model.model_id, {
3795
+ contextWindow: model.context_window,
3796
+ maxOutputTokens: model.max_output_tokens ?? void 0
3797
+ });
3798
+ }
3799
+ })();
3800
+ }
3801
+ await entry.listPromise;
3802
+ }
3803
+
3804
+ // src/sessions/remote_session.ts
3805
+ var RemoteSession = class _RemoteSession {
3806
+ constructor(client, http, sessionData, options = {}) {
3807
+ this.type = "remote";
3808
+ this.messages = [];
3809
+ this.artifacts = /* @__PURE__ */ new Map();
3810
+ this.nextSeq = 1;
3811
+ this.pendingMessages = [];
3812
+ this.currentEvents = [];
3813
+ this.currentUsage = {
3814
+ inputTokens: 0,
3815
+ outputTokens: 0,
3816
+ totalTokens: 0,
3817
+ llmCalls: 0,
3818
+ toolCalls: 0
3819
+ };
3820
+ this.client = client;
3821
+ this.http = http;
3822
+ this.id = asSessionId(sessionData.id);
3823
+ this.metadata = sessionData.metadata;
3824
+ this.customerId = sessionData.customer_id || options.customerId;
3825
+ this.createdAt = new Date(sessionData.created_at);
3826
+ this.updatedAt = new Date(sessionData.updated_at);
3827
+ this.toolRegistry = options.toolRegistry;
3828
+ this.defaultModel = options.defaultModel;
3829
+ this.defaultProvider = options.defaultProvider;
3830
+ this.defaultTools = options.defaultTools;
3831
+ this.resolveModelContext = createModelContextResolver(client);
3832
+ if ("messages" in sessionData && sessionData.messages) {
3833
+ this.messages = sessionData.messages.map((m) => ({
3834
+ type: "message",
3835
+ role: m.role,
3836
+ content: m.content,
3837
+ seq: m.seq,
3838
+ createdAt: new Date(m.created_at),
3839
+ runId: m.run_id ? parseRunId(m.run_id) : void 0
3840
+ }));
3841
+ this.nextSeq = this.messages.length + 1;
3842
+ }
3843
+ }
3844
+ /**
3845
+ * Create a new remote session on the server.
3846
+ *
3847
+ * @param client - ModelRelay client
3848
+ * @param options - Session configuration
3849
+ * @returns A new RemoteSession instance
3850
+ */
3851
+ static async create(client, options = {}) {
3852
+ const http = client.http;
3853
+ const response = await http.request("/sessions", {
3854
+ method: "POST",
3855
+ body: {
3856
+ customer_id: options.customerId,
3857
+ metadata: options.metadata || {}
3858
+ }
3859
+ });
3860
+ const data = await response.json();
3861
+ return new _RemoteSession(client, http, data, options);
3862
+ }
3863
+ /**
3864
+ * Get an existing remote session by ID.
3865
+ *
3866
+ * @param client - ModelRelay client
3867
+ * @param sessionId - ID of the session to retrieve
3868
+ * @param options - Optional configuration (toolRegistry, defaults)
3869
+ * @returns The RemoteSession instance
3870
+ */
3871
+ static async get(client, sessionId, options = {}) {
3872
+ const http = client.http;
3873
+ const id = typeof sessionId === "string" ? sessionId : String(sessionId);
3874
+ const response = await http.request(`/sessions/${id}`, {
3875
+ method: "GET"
3876
+ });
3877
+ const data = await response.json();
3878
+ return new _RemoteSession(client, http, data, options);
3879
+ }
3880
+ /**
3881
+ * List remote sessions.
3882
+ *
3883
+ * @param client - ModelRelay client
3884
+ * @param options - List options
3885
+ * @returns Paginated list of session info
3886
+ */
3887
+ static async list(client, options = {}) {
3888
+ const http = client.http;
3313
3889
  const params = new URLSearchParams();
3314
3890
  if (options.limit) params.set("limit", String(options.limit));
3315
3891
  if (options.offset) params.set("offset", String(options.offset));
@@ -3337,7 +3913,7 @@ var RemoteSession = class _RemoteSession {
3337
3913
  * @param sessionId - ID of the session to delete
3338
3914
  */
3339
3915
  static async delete(client, sessionId) {
3340
- const http = getHTTPClient(client);
3916
+ const http = client.http;
3341
3917
  const id = typeof sessionId === "string" ? sessionId : String(sessionId);
3342
3918
  await http.request(`/sessions/${id}`, {
3343
3919
  method: "DELETE"
@@ -3364,21 +3940,22 @@ var RemoteSession = class _RemoteSession {
3364
3940
  this.resetRunState();
3365
3941
  try {
3366
3942
  const input = await this.buildInput(options);
3367
- const tools = mergeTools2(this.defaultTools, options.tools);
3943
+ const tools = mergeTools(this.defaultTools, options.tools);
3944
+ const mainNodeId = parseNodeId("main");
3368
3945
  const spec = {
3369
3946
  kind: "workflow",
3370
3947
  name: `session-${this.id}-turn-${this.nextSeq}`,
3371
3948
  model: options.model || this.defaultModel,
3372
3949
  nodes: [
3373
3950
  {
3374
- id: "main",
3951
+ id: mainNodeId,
3375
3952
  type: "llm",
3376
3953
  input,
3377
3954
  tools,
3378
3955
  tool_execution: this.toolRegistry ? { mode: "client" } : void 0
3379
3956
  }
3380
3957
  ],
3381
- outputs: [{ name: "result", from: "main" }]
3958
+ outputs: [{ name: parseOutputName("result"), from: mainNodeId }]
3382
3959
  };
3383
3960
  const run = await this.client.runs.create(spec, {
3384
3961
  customerId: options.customerId || this.customerId,
@@ -3391,7 +3968,8 @@ var RemoteSession = class _RemoteSession {
3391
3968
  return {
3392
3969
  status: "error",
3393
3970
  error: error.message,
3394
- runId: this.currentRunId || parseRunId("unknown"),
3971
+ cause: error,
3972
+ runId: this.currentRunId,
3395
3973
  usage: { ...this.currentUsage },
3396
3974
  events: [...this.currentEvents]
3397
3975
  };
@@ -3470,10 +4048,25 @@ var RemoteSession = class _RemoteSession {
3470
4048
  return message;
3471
4049
  }
3472
4050
  async buildInput(options) {
3473
- return buildSessionInputWithContext(
3474
- this.messages,
3475
- options,
3476
- this.defaultModel,
4051
+ const baseInput = messagesToInput(this.messages);
4052
+ if (!options.contextManagement || options.contextManagement === "none") {
4053
+ return baseInput;
4054
+ }
4055
+ const modelId = options.model ?? this.defaultModel;
4056
+ if (!modelId) {
4057
+ throw new ConfigError(
4058
+ "model is required for context management; set options.model or a session defaultModel"
4059
+ );
4060
+ }
4061
+ return prepareInputWithContext(
4062
+ baseInput,
4063
+ {
4064
+ model: modelId,
4065
+ strategy: options.contextManagement,
4066
+ maxHistoryTokens: options.maxHistoryTokens,
4067
+ reserveOutputTokens: options.reserveOutputTokens,
4068
+ onTruncate: options.onContextTruncate
4069
+ },
3477
4070
  this.resolveModelContext
3478
4071
  );
3479
4072
  }
@@ -3482,13 +4075,7 @@ var RemoteSession = class _RemoteSession {
3482
4075
  this.currentNodeId = void 0;
3483
4076
  this.currentWaiting = void 0;
3484
4077
  this.currentEvents = [];
3485
- this.currentUsage = {
3486
- inputTokens: 0,
3487
- outputTokens: 0,
3488
- totalTokens: 0,
3489
- llmCalls: 0,
3490
- toolCalls: 0
3491
- };
4078
+ this.currentUsage = emptyUsage();
3492
4079
  }
3493
4080
  async processRunEvents(signal) {
3494
4081
  if (!this.currentRunId) {
@@ -3727,26 +4314,6 @@ var RemoteSession = class _RemoteSession {
3727
4314
  return text.trim() ? text : void 0;
3728
4315
  }
3729
4316
  };
3730
- function getHTTPClient(client) {
3731
- return client.http;
3732
- }
3733
- function mergeTools2(defaults, overrides) {
3734
- if (!defaults && !overrides) return void 0;
3735
- if (!defaults) return overrides;
3736
- if (!overrides) return defaults;
3737
- const merged = /* @__PURE__ */ new Map();
3738
- for (const tool of defaults) {
3739
- if (tool.type === "function" && tool.function) {
3740
- merged.set(tool.function.name, tool);
3741
- }
3742
- }
3743
- for (const tool of overrides) {
3744
- if (tool.type === "function" && tool.function) {
3745
- merged.set(tool.function.name, tool);
3746
- }
3747
- }
3748
- return Array.from(merged.values());
3749
- }
3750
4317
 
3751
4318
  // src/sessions/client.ts
3752
4319
  var SessionsClient = class {
@@ -3863,111 +4430,1487 @@ var SessionsClient = class {
3863
4430
  customerId: options.customerId
3864
4431
  });
3865
4432
  }
3866
- /**
3867
- * Delete a remote session.
3868
- *
3869
- * Requires a secret key.
3870
- *
3871
- * @param sessionId - ID of the session to delete
3872
- *
3873
- * @example
3874
- * ```typescript
3875
- * await client.sessions.delete("session-id");
3876
- * ```
3877
- */
3878
- async delete(sessionId) {
3879
- return RemoteSession.delete(this.modelRelay, sessionId);
4433
+ /**
4434
+ * Delete a remote session.
4435
+ *
4436
+ * Requires a secret key.
4437
+ *
4438
+ * @param sessionId - ID of the session to delete
4439
+ *
4440
+ * @example
4441
+ * ```typescript
4442
+ * await client.sessions.delete("session-id");
4443
+ * ```
4444
+ */
4445
+ async delete(sessionId) {
4446
+ return RemoteSession.delete(this.modelRelay, sessionId);
4447
+ }
4448
+ };
4449
+
4450
+ // src/tiers.ts
4451
+ function defaultTierModelId(tier) {
4452
+ const def = tier.models.find((m) => m.is_default);
4453
+ if (def) return def.model_id;
4454
+ if (tier.models.length === 1) return tier.models[0].model_id;
4455
+ return void 0;
4456
+ }
4457
+ var TiersClient = class {
4458
+ constructor(http, cfg) {
4459
+ this.http = http;
4460
+ this.apiKey = cfg.apiKey ? parseApiKey(cfg.apiKey) : void 0;
4461
+ this.hasSecretKey = this.apiKey ? isSecretKey(this.apiKey) : false;
4462
+ this.hasAccessToken = !!cfg.accessToken?.trim();
4463
+ }
4464
+ ensureAuth() {
4465
+ if (!this.apiKey && !this.hasAccessToken) {
4466
+ throw new ConfigError(
4467
+ "API key (mr_sk_*) or bearer token required for tier operations"
4468
+ );
4469
+ }
4470
+ }
4471
+ ensureSecretKey() {
4472
+ if (!this.apiKey || !this.hasSecretKey) {
4473
+ throw new ConfigError(
4474
+ "Secret key (mr_sk_*) required for checkout operations"
4475
+ );
4476
+ }
4477
+ }
4478
+ /**
4479
+ * List all tiers in the project.
4480
+ */
4481
+ async list() {
4482
+ this.ensureAuth();
4483
+ const response = await this.http.json("/tiers", {
4484
+ method: "GET",
4485
+ apiKey: this.apiKey
4486
+ });
4487
+ return response.tiers;
4488
+ }
4489
+ /**
4490
+ * Get a tier by ID.
4491
+ */
4492
+ async get(tierId) {
4493
+ this.ensureAuth();
4494
+ if (!tierId?.trim()) {
4495
+ throw new ConfigError("tierId is required");
4496
+ }
4497
+ const response = await this.http.json(`/tiers/${tierId}`, {
4498
+ method: "GET",
4499
+ apiKey: this.apiKey
4500
+ });
4501
+ return response.tier;
4502
+ }
4503
+ /**
4504
+ * Create a Stripe checkout session for a tier (Stripe-first flow).
4505
+ *
4506
+ * This enables users to subscribe before authenticating. Stripe collects
4507
+ * the customer's email during checkout. After checkout completes, a
4508
+ * customer record is created with the email from Stripe. Your backend
4509
+ * can map it to your app user and mint customer tokens as needed.
4510
+ *
4511
+ * Requires a secret key (mr_sk_*).
4512
+ *
4513
+ * @param tierId - The tier ID to create a checkout session for
4514
+ * @param request - Checkout session request with redirect URLs
4515
+ * @returns Checkout session with Stripe URL
4516
+ */
4517
+ async checkout(tierId, request) {
4518
+ this.ensureSecretKey();
4519
+ if (!tierId?.trim()) {
4520
+ throw new ConfigError("tierId is required");
4521
+ }
4522
+ if (!request.success_url?.trim()) {
4523
+ throw new ConfigError("success_url is required");
4524
+ }
4525
+ if (!request.cancel_url?.trim()) {
4526
+ throw new ConfigError("cancel_url is required");
4527
+ }
4528
+ return await this.http.json(
4529
+ `/tiers/${tierId}/checkout`,
4530
+ {
4531
+ method: "POST",
4532
+ apiKey: this.apiKey,
4533
+ body: request
4534
+ }
4535
+ );
4536
+ }
4537
+ };
4538
+
4539
+ // src/plugins.ts
4540
+ import { z as z2 } from "zod";
4541
+
4542
+ // src/tools_user_ask.ts
4543
+ var USER_ASK_TOOL_NAME = "user.ask";
4544
+ var userAskSchema = {
4545
+ type: "object",
4546
+ properties: {
4547
+ question: {
4548
+ type: "string",
4549
+ minLength: 1,
4550
+ description: "The question to ask the user."
4551
+ },
4552
+ options: {
4553
+ type: "array",
4554
+ items: {
4555
+ type: "object",
4556
+ properties: {
4557
+ label: { type: "string", minLength: 1 },
4558
+ description: { type: "string" }
4559
+ },
4560
+ required: ["label"]
4561
+ },
4562
+ description: "Optional multiple choice options."
4563
+ },
4564
+ allow_freeform: {
4565
+ type: "boolean",
4566
+ default: true,
4567
+ description: "Allow user to type a custom response."
4568
+ }
4569
+ },
4570
+ required: ["question"]
4571
+ };
4572
+ function createUserAskTool() {
4573
+ return createFunctionTool(
4574
+ USER_ASK_TOOL_NAME,
4575
+ "Ask the user a clarifying question.",
4576
+ userAskSchema
4577
+ );
4578
+ }
4579
+ function isUserAskToolCall(call) {
4580
+ return call.type === "function" && call.function?.name === USER_ASK_TOOL_NAME;
4581
+ }
4582
+ function parseUserAskArgs(call) {
4583
+ const raw = getToolArgsRaw(call);
4584
+ if (!raw) {
4585
+ throw new ToolArgumentError({
4586
+ message: "user.ask arguments required",
4587
+ toolCallId: call.id,
4588
+ toolName: USER_ASK_TOOL_NAME,
4589
+ rawArguments: raw
4590
+ });
4591
+ }
4592
+ let parsed;
4593
+ try {
4594
+ parsed = JSON.parse(raw);
4595
+ } catch (err) {
4596
+ throw new ToolArgumentError({
4597
+ message: "user.ask arguments must be valid JSON",
4598
+ toolCallId: call.id,
4599
+ toolName: USER_ASK_TOOL_NAME,
4600
+ rawArguments: raw,
4601
+ cause: err
4602
+ });
4603
+ }
4604
+ const question = parsed.question?.trim?.() ?? "";
4605
+ if (!question) {
4606
+ throw new ToolArgumentError({
4607
+ message: "user.ask question required",
4608
+ toolCallId: call.id,
4609
+ toolName: USER_ASK_TOOL_NAME,
4610
+ rawArguments: raw
4611
+ });
4612
+ }
4613
+ if (parsed.options?.length) {
4614
+ for (const opt of parsed.options) {
4615
+ if (!opt?.label?.trim?.()) {
4616
+ throw new ToolArgumentError({
4617
+ message: "user.ask options require label",
4618
+ toolCallId: call.id,
4619
+ toolName: USER_ASK_TOOL_NAME,
4620
+ rawArguments: raw
4621
+ });
4622
+ }
4623
+ }
4624
+ }
4625
+ return {
4626
+ question,
4627
+ options: parsed.options,
4628
+ allow_freeform: parsed.allow_freeform
4629
+ };
4630
+ }
4631
+ function serializeUserAskResult(result) {
4632
+ const answer = result.answer?.trim?.() ?? "";
4633
+ if (!answer) {
4634
+ throw new ToolArgumentError({
4635
+ message: "user.ask answer required",
4636
+ toolCallId: "",
4637
+ toolName: USER_ASK_TOOL_NAME,
4638
+ rawArguments: ""
4639
+ });
4640
+ }
4641
+ return JSON.stringify({ answer, is_freeform: result.is_freeform });
4642
+ }
4643
+ function userAskResultFreeform(answer) {
4644
+ return serializeUserAskResult({ answer, is_freeform: true });
4645
+ }
4646
+ function userAskResultChoice(answer) {
4647
+ return serializeUserAskResult({ answer, is_freeform: false });
4648
+ }
4649
+
4650
+ // src/tools_runner.ts
4651
+ var ToolRunner = class {
4652
+ constructor(options) {
4653
+ this.registry = options.registry;
4654
+ this.runsClient = options.runsClient;
4655
+ this.customerId = options.customerId;
4656
+ this.onBeforeExecute = options.onBeforeExecute;
4657
+ this.onAfterExecute = options.onAfterExecute;
4658
+ this.onSubmitted = options.onSubmitted;
4659
+ this.onUserAsk = options.onUserAsk;
4660
+ this.onError = options.onError;
4661
+ }
4662
+ /**
4663
+ * Handles a node_waiting event by executing tools and submitting results.
4664
+ *
4665
+ * @param runId - The run ID
4666
+ * @param nodeId - The node ID that is waiting
4667
+ * @param waiting - The waiting state with pending tool calls
4668
+ * @returns The submission response with accepted count and new status
4669
+ *
4670
+ * @example
4671
+ * ```typescript
4672
+ * for await (const event of client.runs.events(runId)) {
4673
+ * if (event.type === "node_waiting") {
4674
+ * const result = await runner.handleNodeWaiting(
4675
+ * runId,
4676
+ * event.node_id,
4677
+ * event.waiting
4678
+ * );
4679
+ * console.log(`Submitted ${result.accepted} results, status: ${result.status}`);
4680
+ * }
4681
+ * }
4682
+ * ```
4683
+ */
4684
+ async handleNodeWaiting(runId, nodeId, waiting) {
4685
+ const results = [];
4686
+ for (const pending of waiting.pending_tool_calls) {
4687
+ try {
4688
+ await this.onBeforeExecute?.(pending);
4689
+ const toolCall = createToolCall(
4690
+ pending.tool_call.id,
4691
+ pending.tool_call.name,
4692
+ pending.tool_call.arguments
4693
+ );
4694
+ let result;
4695
+ if (pending.tool_call.name === USER_ASK_TOOL_NAME) {
4696
+ if (!this.onUserAsk) {
4697
+ throw new Error("user.ask requires onUserAsk handler");
4698
+ }
4699
+ const args = parseUserAskArgs(toolCall);
4700
+ const response2 = await this.onUserAsk(pending, args);
4701
+ const output = typeof response2 === "string" ? serializeUserAskResult({ answer: response2, is_freeform: true }) : serializeUserAskResult(response2);
4702
+ result = {
4703
+ toolCallId: pending.tool_call.id,
4704
+ toolName: pending.tool_call.name,
4705
+ result: output,
4706
+ isRetryable: false
4707
+ };
4708
+ } else {
4709
+ result = await this.registry.execute(toolCall);
4710
+ }
4711
+ results.push(result);
4712
+ await this.onAfterExecute?.(result);
4713
+ } catch (err) {
4714
+ const error = err instanceof Error ? err : new Error(String(err));
4715
+ await this.onError?.(error, pending);
4716
+ results.push({
4717
+ toolCallId: pending.tool_call.id,
4718
+ toolName: pending.tool_call.name,
4719
+ result: null,
4720
+ error: error.message
4721
+ });
4722
+ }
4723
+ }
4724
+ const response = await this.runsClient.submitToolResults(
4725
+ runId,
4726
+ {
4727
+ node_id: nodeId,
4728
+ step: waiting.step,
4729
+ request_id: waiting.request_id,
4730
+ results: results.map((r) => ({
4731
+ tool_call: {
4732
+ id: r.toolCallId,
4733
+ name: r.toolName
4734
+ },
4735
+ output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
4736
+ }))
4737
+ },
4738
+ { customerId: this.customerId }
4739
+ );
4740
+ await this.onSubmitted?.(runId, response.accepted, response.status);
4741
+ return {
4742
+ accepted: response.accepted,
4743
+ status: response.status,
4744
+ results
4745
+ };
4746
+ }
4747
+ /**
4748
+ * Processes a stream of run events, automatically handling node_waiting events.
4749
+ *
4750
+ * This is the main entry point for running a workflow with client-side tools.
4751
+ * It yields all events through (including node_waiting after handling).
4752
+ *
4753
+ * @param runId - The run ID to process
4754
+ * @param events - AsyncIterable of run events (from RunsClient.events())
4755
+ * @yields All run events, with node_waiting events handled automatically
4756
+ *
4757
+ * @example
4758
+ * ```typescript
4759
+ * const run = await client.runs.create(workflowSpec);
4760
+ * const eventStream = client.runs.events(run.run_id);
4761
+ *
4762
+ * for await (const event of runner.processEvents(run.run_id, eventStream)) {
4763
+ * switch (event.type) {
4764
+ * case "node_started":
4765
+ * console.log(`Node ${event.node_id} started`);
4766
+ * break;
4767
+ * case "node_succeeded":
4768
+ * console.log(`Node ${event.node_id} succeeded`);
4769
+ * break;
4770
+ * case "run_succeeded":
4771
+ * console.log("Run completed!");
4772
+ * break;
4773
+ * }
4774
+ * }
4775
+ * ```
4776
+ */
4777
+ async *processEvents(runId, events) {
4778
+ for await (const event of events) {
4779
+ if (event.type === "node_waiting") {
4780
+ const waitingEvent = event;
4781
+ try {
4782
+ await this.handleNodeWaiting(
4783
+ runId,
4784
+ waitingEvent.node_id,
4785
+ waitingEvent.waiting
4786
+ );
4787
+ } catch (err) {
4788
+ const error = err instanceof Error ? err : new Error(String(err));
4789
+ await this.onError?.(error);
4790
+ throw error;
4791
+ }
4792
+ }
4793
+ yield event;
4794
+ }
4795
+ }
4796
+ /**
4797
+ * Checks if a run event is a node_waiting event.
4798
+ * Utility for filtering events when not using processEvents().
4799
+ */
4800
+ static isNodeWaiting(event) {
4801
+ return event.type === "node_waiting";
4802
+ }
4803
+ /**
4804
+ * Checks if a run status is terminal (succeeded, failed, or canceled).
4805
+ * Utility for determining when to stop polling.
4806
+ */
4807
+ static isTerminalStatus(status) {
4808
+ return status === "succeeded" || status === "failed" || status === "canceled";
4809
+ }
4810
+ };
4811
+ function createToolRunner(options) {
4812
+ return new ToolRunner(options);
4813
+ }
4814
+
4815
+ // src/plugins.ts
4816
+ var PluginToolNames = {
4817
+ FS_READ_FILE: "fs.read_file",
4818
+ FS_LIST_FILES: "fs.list_files",
4819
+ FS_SEARCH: "fs.search",
4820
+ FS_EDIT: "fs.edit",
4821
+ BASH: "bash",
4822
+ WRITE_FILE: "write_file",
4823
+ USER_ASK: "user.ask"
4824
+ };
4825
+ var OrchestrationModes = {
4826
+ DAG: "dag",
4827
+ Dynamic: "dynamic"
4828
+ };
4829
+ var PluginOrchestrationErrorCodes = {
4830
+ InvalidPlan: "INVALID_PLAN",
4831
+ UnknownAgent: "UNKNOWN_AGENT",
4832
+ MissingDescription: "MISSING_DESCRIPTION",
4833
+ UnknownTool: "UNKNOWN_TOOL",
4834
+ InvalidDependency: "INVALID_DEPENDENCY",
4835
+ InvalidToolConfig: "INVALID_TOOL_CONFIG"
4836
+ };
4837
+ var PluginOrchestrationError = class extends Error {
4838
+ constructor(code, message) {
4839
+ super(`plugin orchestration: ${message}`);
4840
+ this.code = code;
4841
+ }
4842
+ };
4843
+ var DEFAULT_PLUGIN_REF = "HEAD";
4844
+ var DEFAULT_CACHE_TTL_MS = 5 * 6e4;
4845
+ var DEFAULT_GITHUB_API_BASE = "https://api.github.com";
4846
+ var DEFAULT_GITHUB_RAW_BASE = "https://raw.githubusercontent.com";
4847
+ var defaultDynamicToolNames = [
4848
+ PluginToolNames.FS_READ_FILE,
4849
+ PluginToolNames.FS_LIST_FILES,
4850
+ PluginToolNames.FS_SEARCH
4851
+ ];
4852
+ var allowedToolSet = new Set(Object.values(PluginToolNames));
4853
+ var workflowIntentSchema = z2.object({
4854
+ kind: z2.literal(WorkflowKinds.WorkflowIntent),
4855
+ name: z2.string().optional(),
4856
+ model: z2.string().optional(),
4857
+ max_parallelism: z2.number().int().positive().optional(),
4858
+ inputs: z2.array(
4859
+ z2.object({
4860
+ name: z2.string().min(1),
4861
+ type: z2.string().optional(),
4862
+ required: z2.boolean().optional(),
4863
+ description: z2.string().optional(),
4864
+ default: z2.unknown().optional()
4865
+ })
4866
+ ).optional(),
4867
+ nodes: z2.array(
4868
+ z2.object({
4869
+ id: z2.string().min(1),
4870
+ type: z2.enum([
4871
+ WorkflowNodeTypesIntent.LLM,
4872
+ WorkflowNodeTypesIntent.JoinAll,
4873
+ WorkflowNodeTypesIntent.JoinAny,
4874
+ WorkflowNodeTypesIntent.JoinCollect,
4875
+ WorkflowNodeTypesIntent.TransformJSON,
4876
+ WorkflowNodeTypesIntent.MapFanout
4877
+ ]),
4878
+ depends_on: z2.array(z2.string().min(1)).optional(),
4879
+ model: z2.string().optional(),
4880
+ system: z2.string().optional(),
4881
+ user: z2.string().optional(),
4882
+ input: z2.array(z2.unknown()).optional(),
4883
+ stream: z2.boolean().optional(),
4884
+ tools: z2.array(
4885
+ z2.union([
4886
+ z2.string(),
4887
+ z2.object({}).passthrough()
4888
+ ])
4889
+ ).optional(),
4890
+ tool_execution: z2.object({ mode: z2.enum(["server", "client", "agentic"]) }).optional(),
4891
+ limit: z2.number().int().positive().optional(),
4892
+ timeout_ms: z2.number().int().positive().optional(),
4893
+ predicate: z2.object({}).passthrough().optional(),
4894
+ items_from: z2.string().optional(),
4895
+ items_from_input: z2.string().optional(),
4896
+ items_pointer: z2.string().optional(),
4897
+ items_path: z2.string().optional(),
4898
+ subnode: z2.object({}).passthrough().optional(),
4899
+ max_parallelism: z2.number().int().positive().optional(),
4900
+ object: z2.record(z2.unknown()).optional(),
4901
+ merge: z2.array(z2.unknown()).optional()
4902
+ }).passthrough()
4903
+ ).min(1),
4904
+ outputs: z2.array(
4905
+ z2.object({
4906
+ name: z2.string().min(1),
4907
+ from: z2.string().min(1),
4908
+ pointer: z2.string().optional()
4909
+ })
4910
+ ).min(1)
4911
+ }).passthrough();
4912
+ var orchestrationPlanSchema = z2.object({
4913
+ kind: z2.literal("orchestration.plan.v1"),
4914
+ max_parallelism: z2.number().int().positive().optional(),
4915
+ steps: z2.array(
4916
+ z2.object({
4917
+ id: z2.string().min(1).optional(),
4918
+ depends_on: z2.array(z2.string().min(1)).optional(),
4919
+ agents: z2.array(
4920
+ z2.object({
4921
+ id: z2.string().min(1),
4922
+ reason: z2.string().min(1)
4923
+ })
4924
+ ).min(1)
4925
+ }).strict()
4926
+ ).min(1)
4927
+ }).strict();
4928
+ var PluginLoader = class {
4929
+ constructor(options = {}) {
4930
+ this.cache = /* @__PURE__ */ new Map();
4931
+ this.fetchFn = options.fetch || globalThis.fetch;
4932
+ if (!this.fetchFn) {
4933
+ throw new ConfigError("fetch is required to load plugins");
4934
+ }
4935
+ this.apiBaseUrl = options.apiBaseUrl || DEFAULT_GITHUB_API_BASE;
4936
+ this.rawBaseUrl = options.rawBaseUrl || DEFAULT_GITHUB_RAW_BASE;
4937
+ this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
4938
+ this.now = options.now || (() => /* @__PURE__ */ new Date());
4939
+ }
4940
+ async load(sourceUrl, options = {}) {
4941
+ const ref = parseGitHubPluginRef(sourceUrl);
4942
+ const key = ref.canonical;
4943
+ const cached = this.cache.get(key);
4944
+ if (cached && cached.expiresAt > this.now().getTime()) {
4945
+ return clonePlugin(cached.plugin);
4946
+ }
4947
+ const manifestCandidates = ["PLUGIN.md", "SKILL.md"];
4948
+ let manifestPath = "";
4949
+ let manifestMd = "";
4950
+ for (const candidate of manifestCandidates) {
4951
+ const path = joinRepoPath(ref.repoPath, candidate);
4952
+ const url = this.rawUrl(ref, path);
4953
+ const res = await this.fetchText(url, options.signal);
4954
+ if (res.status === 404) {
4955
+ continue;
4956
+ }
4957
+ if (!res.ok) {
4958
+ throw new ConfigError(`fetch ${path}: ${res.statusText}`);
4959
+ }
4960
+ manifestPath = path;
4961
+ manifestMd = res.body;
4962
+ break;
4963
+ }
4964
+ if (!manifestPath) {
4965
+ throw new ConfigError("plugin manifest not found");
4966
+ }
4967
+ const commandsDir = joinRepoPath(ref.repoPath, "commands");
4968
+ const agentsDir = joinRepoPath(ref.repoPath, "agents");
4969
+ const commandFiles = await this.listMarkdownFiles(ref, commandsDir, options.signal);
4970
+ const agentFiles = await this.listMarkdownFiles(ref, agentsDir, options.signal);
4971
+ const plugin = {
4972
+ id: asPluginId(`${ref.owner}/${ref.repo}${ref.repoPath ? `/${ref.repoPath}` : ""}`),
4973
+ url: asPluginUrl(ref.canonical),
4974
+ manifest: parsePluginManifest(manifestMd),
4975
+ commands: {},
4976
+ agents: {},
4977
+ rawFiles: { [manifestPath]: manifestMd },
4978
+ ref: {
4979
+ owner: ref.owner,
4980
+ repo: ref.repo,
4981
+ ref: ref.ref,
4982
+ path: ref.repoPath || void 0
4983
+ },
4984
+ loadedAt: this.now()
4985
+ };
4986
+ for (const filePath of commandFiles) {
4987
+ const res = await this.fetchText(this.rawUrl(ref, filePath), options.signal);
4988
+ if (!res.ok) {
4989
+ throw new ConfigError(`fetch ${filePath}: ${res.statusText}`);
4990
+ }
4991
+ const { tools, body } = parseMarkdownFrontMatter(res.body);
4992
+ const name = asPluginCommandName(basename(filePath));
4993
+ plugin.commands[String(name)] = {
4994
+ name,
4995
+ prompt: body,
4996
+ agentRefs: extractAgentRefs(body),
4997
+ tools
4998
+ };
4999
+ plugin.rawFiles[filePath] = res.body;
5000
+ }
5001
+ for (const filePath of agentFiles) {
5002
+ const res = await this.fetchText(this.rawUrl(ref, filePath), options.signal);
5003
+ if (!res.ok) {
5004
+ throw new ConfigError(`fetch ${filePath}: ${res.statusText}`);
5005
+ }
5006
+ const { description, tools, body } = parseMarkdownFrontMatter(res.body);
5007
+ const name = asPluginAgentName(basename(filePath));
5008
+ plugin.agents[String(name)] = {
5009
+ name,
5010
+ systemPrompt: body,
5011
+ description,
5012
+ tools
5013
+ };
5014
+ plugin.rawFiles[filePath] = res.body;
5015
+ }
5016
+ plugin.manifest.commands = sortedKeys(Object.values(plugin.commands).map((c) => c.name));
5017
+ plugin.manifest.agents = sortedKeys(Object.values(plugin.agents).map((a) => a.name));
5018
+ this.cache.set(key, {
5019
+ expiresAt: this.now().getTime() + this.cacheTtlMs,
5020
+ plugin: clonePlugin(plugin)
5021
+ });
5022
+ return clonePlugin(plugin);
5023
+ }
5024
+ async listMarkdownFiles(ref, repoDir, signal) {
5025
+ const path = `/repos/${ref.owner}/${ref.repo}/contents/${repoDir}`;
5026
+ const url = `${this.apiBaseUrl}${path}?ref=${encodeURIComponent(ref.ref)}`;
5027
+ const res = await this.fetchJson(url, signal);
5028
+ if (res.status === 404) {
5029
+ return [];
5030
+ }
5031
+ if (!res.ok) {
5032
+ throw new ConfigError(`fetch ${repoDir}: ${res.statusText}`);
5033
+ }
5034
+ return res.body.filter((entry) => entry.type === "file" && entry.name.endsWith(".md")).map((entry) => entry.path);
5035
+ }
5036
+ rawUrl(ref, repoPath) {
5037
+ const cleaned = repoPath.replace(/^\/+/, "");
5038
+ return `${this.rawBaseUrl}/${ref.owner}/${ref.repo}/${ref.ref}/${cleaned}`;
5039
+ }
5040
+ async fetchText(url, signal) {
5041
+ const res = await this.fetchFn(url, { signal });
5042
+ const body = await res.text();
5043
+ return { ok: res.ok, status: res.status, statusText: res.statusText, body };
5044
+ }
5045
+ async fetchJson(url, signal) {
5046
+ const res = await this.fetchFn(url, { signal });
5047
+ if (!res.ok) {
5048
+ return {
5049
+ ok: res.ok,
5050
+ status: res.status,
5051
+ statusText: res.statusText,
5052
+ body: []
5053
+ };
5054
+ }
5055
+ const body = await res.json();
5056
+ return { ok: res.ok, status: res.status, statusText: res.statusText, body };
5057
+ }
5058
+ };
5059
+ var PluginConverter = class {
5060
+ constructor(responses, http, auth, options = {}) {
5061
+ this.responses = responses;
5062
+ this.http = http;
5063
+ this.auth = auth;
5064
+ this.converterModel = asModelId(options.converterModel || "claude-3-5-haiku-latest");
5065
+ }
5066
+ async toWorkflow(plugin, commandName, task) {
5067
+ const command = resolveCommand(plugin, commandName);
5068
+ const prompt = buildPluginConversionPrompt(plugin, command, task);
5069
+ const schemaName = "workflow";
5070
+ const result = await this.responses.object({
5071
+ model: this.converterModel,
5072
+ schema: workflowIntentSchema,
5073
+ schemaName,
5074
+ system: pluginToWorkflowSystemPrompt,
5075
+ prompt
5076
+ });
5077
+ const spec = normalizeWorkflowIntent(result);
5078
+ validateWorkflowTools(spec);
5079
+ return spec;
5080
+ }
5081
+ async toWorkflowDynamic(plugin, commandName, task) {
5082
+ const command = resolveCommand(plugin, commandName);
5083
+ const { candidates, lookup } = buildOrchestrationCandidates(plugin, command);
5084
+ const prompt = buildPluginOrchestrationPrompt(plugin, command, task, candidates);
5085
+ const plan = await this.responses.object({
5086
+ model: this.converterModel,
5087
+ schema: orchestrationPlanSchema,
5088
+ schemaName: "orchestration_plan",
5089
+ system: pluginOrchestrationSystemPrompt,
5090
+ prompt
5091
+ });
5092
+ validateOrchestrationPlan(plan, lookup);
5093
+ const spec = buildDynamicWorkflowFromPlan(plugin, command, task, plan, lookup, this.converterModel);
5094
+ if (specRequiresTools(spec)) {
5095
+ await ensureModelSupportsTools(this.http, this.auth, this.converterModel);
5096
+ }
5097
+ validateWorkflowTools(spec);
5098
+ return spec;
5099
+ }
5100
+ };
5101
+ var PluginRunner = class {
5102
+ constructor(runs) {
5103
+ this.runs = runs;
5104
+ }
5105
+ async run(spec, config) {
5106
+ const created = await this.runs.create(spec, config.runOptions);
5107
+ return this.wait(created.run_id, config);
5108
+ }
5109
+ async wait(runId, config) {
5110
+ const events = [];
5111
+ const toolRegistry = config.toolRegistry;
5112
+ const eventStream = await this.runs.events(runId, config.runOptions);
5113
+ const runner = toolRegistry ? new ToolRunner({ registry: toolRegistry, runsClient: this.runs }) : null;
5114
+ const stream = runner ? runner.processEvents(runId, eventStream) : eventStream;
5115
+ let terminal = null;
5116
+ for await (const event of stream) {
5117
+ events.push(event);
5118
+ if (event.type === "run_completed") {
5119
+ terminal = "succeeded";
5120
+ break;
5121
+ }
5122
+ if (event.type === "run_failed") {
5123
+ terminal = "failed";
5124
+ break;
5125
+ }
5126
+ if (event.type === "run_canceled") {
5127
+ terminal = "canceled";
5128
+ break;
5129
+ }
5130
+ }
5131
+ const snapshot = await this.runs.get(runId, config.runOptions);
5132
+ return {
5133
+ runId: snapshot.run_id,
5134
+ status: terminal || snapshot.status,
5135
+ outputs: snapshot.outputs,
5136
+ costSummary: snapshot.cost_summary,
5137
+ events
5138
+ };
5139
+ }
5140
+ };
5141
+ var PluginsClient = class {
5142
+ constructor(deps) {
5143
+ this.responses = deps.responses;
5144
+ this.http = deps.http;
5145
+ this.auth = deps.auth;
5146
+ this.loader = new PluginLoader(deps.options);
5147
+ this.converter = new PluginConverter(deps.responses, deps.http, deps.auth);
5148
+ this.runner = new PluginRunner(deps.runs);
5149
+ }
5150
+ load(url, options) {
5151
+ return this.loader.load(url, options);
5152
+ }
5153
+ async run(plugin, command, config) {
5154
+ const task = config.userTask?.trim();
5155
+ if (!task) {
5156
+ throw new ConfigError("userTask is required");
5157
+ }
5158
+ const mode = normalizeOrchestrationMode(config.orchestrationMode);
5159
+ const converterModel = config.converterModel ? asModelId(String(config.converterModel)) : void 0;
5160
+ const converter = converterModel ? new PluginConverter(this.responses, this.http, this.auth, {
5161
+ converterModel
5162
+ }) : this.converter;
5163
+ let spec;
5164
+ if (mode === OrchestrationModes.Dynamic) {
5165
+ spec = await converter.toWorkflowDynamic(plugin, command, task);
5166
+ } else {
5167
+ spec = await converter.toWorkflow(plugin, command, task);
5168
+ }
5169
+ if (config.model) {
5170
+ spec = { ...spec, model: String(config.model) };
5171
+ }
5172
+ return this.runner.run(spec, config);
5173
+ }
5174
+ async quickRun(pluginUrl, command, userTask, config = {}) {
5175
+ const plugin = await this.load(pluginUrl);
5176
+ return this.run(plugin, command, { ...config, userTask });
5177
+ }
5178
+ };
5179
+ function normalizeOrchestrationMode(mode) {
5180
+ if (!mode) return OrchestrationModes.DAG;
5181
+ if (mode !== OrchestrationModes.DAG && mode !== OrchestrationModes.Dynamic) {
5182
+ throw new ConfigError(`invalid orchestration mode: ${mode}`);
5183
+ }
5184
+ return mode;
5185
+ }
5186
+ function resolveCommand(plugin, commandName) {
5187
+ const trimmed = commandName.trim();
5188
+ if (!trimmed) {
5189
+ throw new ConfigError("command is required");
5190
+ }
5191
+ const command = plugin.commands[trimmed];
5192
+ if (!command) {
5193
+ throw new ConfigError("unknown command");
5194
+ }
5195
+ return command;
5196
+ }
5197
+ var pluginToWorkflowSystemPrompt = `You convert a ModelRelay plugin (markdown files) into a single workflow JSON spec.
5198
+
5199
+ Rules:
5200
+ - Output MUST be a single JSON object and MUST validate as workflow.
5201
+ - Do NOT output markdown, commentary, or code fences.
5202
+ - Use a DAG with parallelism when multiple agents are independent.
5203
+ - Use join.all to aggregate parallel branches and then a final synthesizer node.
5204
+ - Use depends_on for edges between nodes.
5205
+ - Bind node outputs using {{placeholders}} when passing data forward.
5206
+ - Tool contract:
5207
+ - Target tools.v0 client tools (see docs/reference/tools.md).
5208
+ - Workspace access MUST use these exact function tool names:
5209
+ - ${Object.values(PluginToolNames).join(", ")}
5210
+ - Prefer fs.* tools for reading/listing/searching the workspace (use bash only when necessary).
5211
+ - Do NOT invent ad-hoc tool names (no repo.*, github.*, filesystem.*, etc.).
5212
+ - All client tools MUST be represented as type="function" tools.
5213
+ - Any node that includes tools MUST set tool_execution.mode="client".
5214
+ - Prefer minimal nodes needed to satisfy the task.
5215
+ `;
5216
+ var pluginOrchestrationSystemPrompt = `You plan which plugin agents to run based only on their descriptions.
5217
+
5218
+ Rules:
5219
+ - Output MUST be a single JSON object that matches orchestration.plan.v1.
5220
+ - Do NOT output markdown, commentary, or code fences.
5221
+ - Select only from the provided agent IDs.
5222
+ - Prefer minimal agents needed to satisfy the user task.
5223
+ - Use multiple steps only when later agents must build on earlier results.
5224
+ - Each step can run agents in parallel.
5225
+ - Use "id" + "depends_on" if you need non-sequential step ordering.
5226
+ `;
5227
+ function buildPluginConversionPrompt(plugin, command, userTask) {
5228
+ const out = [];
5229
+ out.push(`PLUGIN_URL: ${plugin.url}`);
5230
+ out.push(`COMMAND: ${command.name}`);
5231
+ out.push("USER_TASK:");
5232
+ out.push(userTask.trim());
5233
+ out.push("");
5234
+ out.push(`PLUGIN_MANIFEST:`);
5235
+ out.push(JSON.stringify(plugin.manifest));
5236
+ out.push("");
5237
+ out.push(`COMMAND_MARKDOWN (commands/${command.name}.md):`);
5238
+ out.push(command.prompt);
5239
+ out.push("");
5240
+ const agentNames = Object.keys(plugin.agents).sort();
5241
+ if (agentNames.length) {
5242
+ out.push("AGENTS_MARKDOWN:");
5243
+ for (const name of agentNames) {
5244
+ out.push(`---- agents/${name}.md ----`);
5245
+ out.push(plugin.agents[name].systemPrompt);
5246
+ out.push("");
5247
+ }
5248
+ }
5249
+ return out.join("\n");
5250
+ }
5251
+ function buildOrchestrationCandidates(plugin, command) {
5252
+ const names = command.agentRefs?.length ? command.agentRefs : Object.values(plugin.agents).map((agent) => agent.name);
5253
+ if (!names.length) {
5254
+ throw new PluginOrchestrationError(
5255
+ PluginOrchestrationErrorCodes.InvalidPlan,
5256
+ "no agents available for dynamic orchestration"
5257
+ );
5258
+ }
5259
+ const lookup = /* @__PURE__ */ new Map();
5260
+ const candidates = [];
5261
+ for (const name of names) {
5262
+ const agent = plugin.agents[String(name)];
5263
+ if (!agent) {
5264
+ throw new PluginOrchestrationError(
5265
+ PluginOrchestrationErrorCodes.UnknownAgent,
5266
+ `agent "${name}" not found`
5267
+ );
5268
+ }
5269
+ const desc = agent.description?.trim();
5270
+ if (!desc) {
5271
+ throw new PluginOrchestrationError(
5272
+ PluginOrchestrationErrorCodes.MissingDescription,
5273
+ `agent "${name}" missing description`
5274
+ );
5275
+ }
5276
+ lookup.set(String(name), agent);
5277
+ candidates.push({ name, description: desc, agent });
5278
+ }
5279
+ return { candidates, lookup };
5280
+ }
5281
+ function buildPluginOrchestrationPrompt(plugin, command, userTask, candidates) {
5282
+ const out = [];
5283
+ if (plugin.manifest.name) {
5284
+ out.push(`PLUGIN_NAME: ${plugin.manifest.name}`);
5285
+ }
5286
+ if (plugin.manifest.description) {
5287
+ out.push(`PLUGIN_DESCRIPTION: ${plugin.manifest.description}`);
5288
+ }
5289
+ out.push(`COMMAND: ${command.name}`);
5290
+ out.push("USER_TASK:");
5291
+ out.push(userTask.trim());
5292
+ out.push("");
5293
+ if (command.prompt.trim()) {
5294
+ out.push("COMMAND_MARKDOWN:");
5295
+ out.push(command.prompt);
5296
+ out.push("");
5297
+ }
5298
+ out.push("CANDIDATE_AGENTS:");
5299
+ for (const c of candidates) {
5300
+ out.push(`- id: ${c.name}`);
5301
+ out.push(` description: ${c.description}`);
5302
+ }
5303
+ return out.join("\n");
5304
+ }
5305
+ function validateOrchestrationPlan(plan, lookup) {
5306
+ if (plan.max_parallelism && plan.max_parallelism < 1) {
5307
+ throw new PluginOrchestrationError(
5308
+ PluginOrchestrationErrorCodes.InvalidPlan,
5309
+ "max_parallelism must be >= 1"
5310
+ );
5311
+ }
5312
+ const stepIds = /* @__PURE__ */ new Map();
5313
+ let hasExplicitDeps = false;
5314
+ plan.steps.forEach((step, idx) => {
5315
+ if (step.depends_on?.length) {
5316
+ hasExplicitDeps = true;
5317
+ }
5318
+ if (step.id) {
5319
+ const key = step.id.trim();
5320
+ if (stepIds.has(key)) {
5321
+ throw new PluginOrchestrationError(
5322
+ PluginOrchestrationErrorCodes.InvalidPlan,
5323
+ `duplicate step id "${key}"`
5324
+ );
5325
+ }
5326
+ stepIds.set(key, idx);
5327
+ }
5328
+ });
5329
+ if (hasExplicitDeps) {
5330
+ plan.steps.forEach((step) => {
5331
+ if (!step.id?.trim()) {
5332
+ throw new PluginOrchestrationError(
5333
+ PluginOrchestrationErrorCodes.InvalidPlan,
5334
+ "step id required when depends_on is used"
5335
+ );
5336
+ }
5337
+ });
5338
+ }
5339
+ const seen = /* @__PURE__ */ new Set();
5340
+ plan.steps.forEach((step, idx) => {
5341
+ if (!step.agents.length) {
5342
+ throw new PluginOrchestrationError(
5343
+ PluginOrchestrationErrorCodes.InvalidPlan,
5344
+ `step ${idx + 1} must include at least one agent`
5345
+ );
5346
+ }
5347
+ if (step.depends_on) {
5348
+ for (const dep of step.depends_on) {
5349
+ const depId = dep.trim();
5350
+ const depIndex = depId ? stepIds.get(depId) : void 0;
5351
+ if (!depId) {
5352
+ throw new PluginOrchestrationError(
5353
+ PluginOrchestrationErrorCodes.InvalidDependency,
5354
+ `step ${idx + 1} has empty depends_on`
5355
+ );
5356
+ }
5357
+ if (depIndex === void 0) {
5358
+ throw new PluginOrchestrationError(
5359
+ PluginOrchestrationErrorCodes.InvalidDependency,
5360
+ `step ${idx + 1} depends on unknown step "${depId}"`
5361
+ );
5362
+ }
5363
+ if (depIndex >= idx) {
5364
+ throw new PluginOrchestrationError(
5365
+ PluginOrchestrationErrorCodes.InvalidDependency,
5366
+ `step ${idx + 1} depends on future step "${depId}"`
5367
+ );
5368
+ }
5369
+ }
5370
+ }
5371
+ for (const agent of step.agents) {
5372
+ const id = agent.id.trim();
5373
+ if (!id) {
5374
+ throw new PluginOrchestrationError(
5375
+ PluginOrchestrationErrorCodes.InvalidPlan,
5376
+ `step ${idx + 1} agent id required`
5377
+ );
5378
+ }
5379
+ if (!lookup.has(id)) {
5380
+ throw new PluginOrchestrationError(
5381
+ PluginOrchestrationErrorCodes.UnknownAgent,
5382
+ `unknown agent "${id}"`
5383
+ );
5384
+ }
5385
+ if (!agent.reason?.trim()) {
5386
+ throw new PluginOrchestrationError(
5387
+ PluginOrchestrationErrorCodes.InvalidPlan,
5388
+ `agent "${id}" must include a reason`
5389
+ );
5390
+ }
5391
+ if (seen.has(id)) {
5392
+ throw new PluginOrchestrationError(
5393
+ PluginOrchestrationErrorCodes.InvalidPlan,
5394
+ `agent "${id}" referenced more than once`
5395
+ );
5396
+ }
5397
+ seen.add(id);
5398
+ }
5399
+ });
5400
+ }
5401
+ function buildDynamicWorkflowFromPlan(plugin, command, userTask, plan, lookup, model) {
5402
+ const stepKeys = plan.steps.map((step, idx) => step.id?.trim() || `step_${idx + 1}`);
5403
+ const stepOrder = new Map(stepKeys.map((key, idx) => [key, idx]));
5404
+ const stepOutputs = /* @__PURE__ */ new Map();
5405
+ const usedNodeIds = /* @__PURE__ */ new Set();
5406
+ const nodes = [];
5407
+ const hasExplicitDeps = plan.steps.some((step) => (step.depends_on?.length ?? 0) > 0);
5408
+ for (let i = 0; i < plan.steps.length; i += 1) {
5409
+ const step = plan.steps[i];
5410
+ const stepKey = stepKeys[i];
5411
+ const dependencyKeys = hasExplicitDeps ? step.depends_on || [] : i > 0 ? [stepKeys[i - 1]] : [];
5412
+ const deps = dependencyKeys.map((raw) => {
5413
+ const key = raw.trim();
5414
+ const nodeId = stepOutputs.get(key);
5415
+ if (!nodeId) {
5416
+ throw new PluginOrchestrationError(
5417
+ PluginOrchestrationErrorCodes.InvalidDependency,
5418
+ `missing output for dependency "${key}"`
5419
+ );
5420
+ }
5421
+ const depIndex = stepOrder.get(key);
5422
+ if (depIndex === void 0 || depIndex >= i) {
5423
+ throw new PluginOrchestrationError(
5424
+ PluginOrchestrationErrorCodes.InvalidDependency,
5425
+ `invalid dependency "${key}"`
5426
+ );
5427
+ }
5428
+ return { stepId: key, nodeId };
5429
+ });
5430
+ const stepNodeIds = [];
5431
+ for (const selection of step.agents) {
5432
+ const agentName = selection.id.trim();
5433
+ const agent = lookup.get(agentName);
5434
+ if (!agent) {
5435
+ throw new PluginOrchestrationError(
5436
+ PluginOrchestrationErrorCodes.UnknownAgent,
5437
+ `unknown agent "${agentName}"`
5438
+ );
5439
+ }
5440
+ const nodeId = parseNodeId(formatAgentNodeId(agentName));
5441
+ if (usedNodeIds.has(nodeId)) {
5442
+ throw new PluginOrchestrationError(
5443
+ PluginOrchestrationErrorCodes.InvalidPlan,
5444
+ `duplicate node id "${nodeId}"`
5445
+ );
5446
+ }
5447
+ const tools = buildToolRefs(agent, command);
5448
+ const node = {
5449
+ id: nodeId,
5450
+ type: WorkflowNodeTypesIntent.LLM,
5451
+ system: agent.systemPrompt.trim() || void 0,
5452
+ user: buildDynamicAgentUserPrompt(command, userTask, deps),
5453
+ depends_on: deps.length ? deps.map((d) => d.nodeId) : void 0,
5454
+ tools: tools.length ? tools : void 0,
5455
+ tool_execution: tools.length ? { mode: "client" } : void 0
5456
+ };
5457
+ nodes.push(node);
5458
+ stepNodeIds.push(nodeId);
5459
+ usedNodeIds.add(nodeId);
5460
+ }
5461
+ let outputNodeId = stepNodeIds[0];
5462
+ if (stepNodeIds.length > 1) {
5463
+ const joinId = parseNodeId(formatStepJoinNodeId(stepKey));
5464
+ if (usedNodeIds.has(joinId)) {
5465
+ throw new PluginOrchestrationError(
5466
+ PluginOrchestrationErrorCodes.InvalidPlan,
5467
+ `duplicate node id "${joinId}"`
5468
+ );
5469
+ }
5470
+ nodes.push({
5471
+ id: joinId,
5472
+ type: WorkflowNodeTypesIntent.JoinAll,
5473
+ depends_on: stepNodeIds
5474
+ });
5475
+ usedNodeIds.add(joinId);
5476
+ outputNodeId = joinId;
5477
+ }
5478
+ stepOutputs.set(stepKey, outputNodeId);
5479
+ }
5480
+ const terminalOutputs = findTerminalOutputs(stepKeys, plan, stepOutputs, hasExplicitDeps);
5481
+ const synthId = parseNodeId("orchestrator_synthesize");
5482
+ const synthNode = {
5483
+ id: synthId,
5484
+ type: WorkflowNodeTypesIntent.LLM,
5485
+ user: buildDynamicSynthesisPrompt(command, userTask, terminalOutputs),
5486
+ depends_on: terminalOutputs
5487
+ };
5488
+ const spec = {
5489
+ kind: WorkflowKinds.WorkflowIntent,
5490
+ name: plugin.manifest.name?.trim() || command.name,
5491
+ model: String(model),
5492
+ max_parallelism: plan.max_parallelism,
5493
+ nodes: [...nodes, synthNode],
5494
+ outputs: [{ name: parseOutputName("result"), from: synthId }]
5495
+ };
5496
+ return spec;
5497
+ }
5498
+ function buildDynamicAgentUserPrompt(command, task, deps) {
5499
+ const parts = [];
5500
+ if (command.prompt.trim()) {
5501
+ parts.push(command.prompt.trim());
5502
+ }
5503
+ parts.push("USER_TASK:");
5504
+ parts.push(task.trim());
5505
+ if (deps.length) {
5506
+ parts.push("", "PREVIOUS_STEP_OUTPUTS:");
5507
+ for (const dep of deps) {
5508
+ parts.push(`- ${dep.stepId}: {{${dep.nodeId}}}`);
5509
+ }
3880
5510
  }
3881
- };
3882
-
3883
- // src/tiers.ts
3884
- function defaultTierModelId(tier) {
3885
- const def = tier.models.find((m) => m.is_default);
3886
- if (def) return def.model_id;
3887
- if (tier.models.length === 1) return tier.models[0].model_id;
3888
- return void 0;
5511
+ return parts.join("\n");
3889
5512
  }
3890
- var TiersClient = class {
3891
- constructor(http, cfg) {
3892
- this.http = http;
3893
- this.apiKey = cfg.apiKey ? parseApiKey(cfg.apiKey) : void 0;
3894
- this.hasSecretKey = this.apiKey ? isSecretKey(this.apiKey) : false;
3895
- this.hasAccessToken = !!cfg.accessToken?.trim();
5513
+ function buildDynamicSynthesisPrompt(command, task, outputs) {
5514
+ const parts = ["Synthesize the results and complete the task."];
5515
+ if (command.prompt.trim()) {
5516
+ parts.push("", "COMMAND:");
5517
+ parts.push(command.prompt.trim());
3896
5518
  }
3897
- ensureAuth() {
3898
- if (!this.apiKey && !this.hasAccessToken) {
3899
- throw new ConfigError(
3900
- "API key (mr_sk_*) or bearer token required for tier operations"
3901
- );
5519
+ parts.push("", "USER_TASK:");
5520
+ parts.push(task.trim());
5521
+ if (outputs.length) {
5522
+ parts.push("", "RESULTS:");
5523
+ for (const id of outputs) {
5524
+ parts.push(`- {{${id}}}`);
3902
5525
  }
3903
5526
  }
3904
- ensureSecretKey() {
3905
- if (!this.apiKey || !this.hasSecretKey) {
3906
- throw new ConfigError(
3907
- "Secret key (mr_sk_*) required for checkout operations"
5527
+ return parts.join("\n");
5528
+ }
5529
+ function buildToolRefs(agent, command) {
5530
+ const names = agent.tools?.length ? agent.tools : command.tools?.length ? command.tools : defaultDynamicToolNames;
5531
+ const unique = /* @__PURE__ */ new Set();
5532
+ for (const name of names) {
5533
+ if (!allowedToolSet.has(name)) {
5534
+ throw new PluginOrchestrationError(
5535
+ PluginOrchestrationErrorCodes.UnknownTool,
5536
+ `unknown tool "${name}"`
3908
5537
  );
3909
5538
  }
5539
+ unique.add(name);
3910
5540
  }
3911
- /**
3912
- * List all tiers in the project.
3913
- */
3914
- async list() {
3915
- this.ensureAuth();
3916
- const response = await this.http.json("/tiers", {
3917
- method: "GET",
3918
- apiKey: this.apiKey
3919
- });
3920
- return response.tiers;
5541
+ return Array.from(unique.values());
5542
+ }
5543
+ function findTerminalOutputs(stepKeys, plan, outputs, explicit) {
5544
+ if (!explicit) {
5545
+ const lastKey = stepKeys[stepKeys.length - 1];
5546
+ const out = outputs.get(lastKey);
5547
+ return out ? [out] : [];
5548
+ }
5549
+ const depended = /* @__PURE__ */ new Set();
5550
+ plan.steps.forEach((step) => {
5551
+ (step.depends_on || []).forEach((dep) => depended.add(dep.trim()));
5552
+ });
5553
+ return stepKeys.filter((key) => !depended.has(key)).map((key) => outputs.get(key)).filter((value) => Boolean(value));
5554
+ }
5555
+ function formatAgentNodeId(name) {
5556
+ const token = sanitizeNodeToken(name);
5557
+ if (!token) {
5558
+ throw new PluginOrchestrationError(
5559
+ PluginOrchestrationErrorCodes.InvalidPlan,
5560
+ "agent id must contain alphanumeric characters"
5561
+ );
3921
5562
  }
3922
- /**
3923
- * Get a tier by ID.
3924
- */
3925
- async get(tierId) {
3926
- this.ensureAuth();
3927
- if (!tierId?.trim()) {
3928
- throw new ConfigError("tierId is required");
5563
+ return `agent_${token}`;
5564
+ }
5565
+ function formatStepJoinNodeId(stepKey) {
5566
+ const token = sanitizeNodeToken(stepKey);
5567
+ if (!token) {
5568
+ throw new PluginOrchestrationError(
5569
+ PluginOrchestrationErrorCodes.InvalidPlan,
5570
+ "step id must contain alphanumeric characters"
5571
+ );
5572
+ }
5573
+ return token.startsWith("step_") ? `${token}_join` : `step_${token}_join`;
5574
+ }
5575
+ function sanitizeNodeToken(raw) {
5576
+ const trimmed = raw.trim();
5577
+ if (!trimmed) return "";
5578
+ let out = "";
5579
+ for (const ch of trimmed) {
5580
+ if (/[a-zA-Z0-9]/.test(ch)) {
5581
+ out += ch.toLowerCase();
5582
+ } else {
5583
+ out += "_";
3929
5584
  }
3930
- const response = await this.http.json(`/tiers/${tierId}`, {
5585
+ }
5586
+ out = out.replace(/_+/g, "_");
5587
+ return out.replace(/^_+|_+$/g, "");
5588
+ }
5589
+ async function ensureModelSupportsTools(http, auth, model) {
5590
+ const authHeaders = await auth.authForResponses();
5591
+ const query = encodeURIComponent("tools");
5592
+ const response = await http.json(
5593
+ `/models?capability=${query}`,
5594
+ {
3931
5595
  method: "GET",
3932
- apiKey: this.apiKey
3933
- });
3934
- return response.tier;
5596
+ apiKey: authHeaders.apiKey,
5597
+ accessToken: authHeaders.accessToken
5598
+ }
5599
+ );
5600
+ const modelId = String(model).trim();
5601
+ const found = response.models?.some((entry) => entry.model_id?.trim() === modelId);
5602
+ if (!found) {
5603
+ throw new PluginOrchestrationError(
5604
+ PluginOrchestrationErrorCodes.InvalidToolConfig,
5605
+ `model "${modelId}" does not support tool calling`
5606
+ );
3935
5607
  }
3936
- /**
3937
- * Create a Stripe checkout session for a tier (Stripe-first flow).
3938
- *
3939
- * This enables users to subscribe before authenticating. Stripe collects
3940
- * the customer's email during checkout. After checkout completes, a
3941
- * customer record is created with the email from Stripe. Your backend
3942
- * can map it to your app user and mint customer tokens as needed.
3943
- *
3944
- * Requires a secret key (mr_sk_*).
3945
- *
3946
- * @param tierId - The tier ID to create a checkout session for
3947
- * @param request - Checkout session request with redirect URLs
3948
- * @returns Checkout session with Stripe URL
3949
- */
3950
- async checkout(tierId, request) {
3951
- this.ensureSecretKey();
3952
- if (!tierId?.trim()) {
3953
- throw new ConfigError("tierId is required");
5608
+ }
5609
+ function normalizeWorkflowIntent(spec) {
5610
+ const validation = validateWithZod(workflowIntentSchema, spec);
5611
+ if (!validation.success) {
5612
+ throw new ConfigError("workflow intent validation failed");
5613
+ }
5614
+ validateWorkflowTools(spec);
5615
+ return spec;
5616
+ }
5617
+ function validateWorkflowTools(spec) {
5618
+ for (const node of spec.nodes) {
5619
+ if (node.type !== WorkflowNodeTypesIntent.LLM || !node.tools?.length) {
5620
+ continue;
5621
+ }
5622
+ const tools = node.tools || [];
5623
+ let mode = node.tool_execution?.mode;
5624
+ for (const tool of tools) {
5625
+ if (typeof tool !== "string") {
5626
+ throw new ConfigError(`plugin conversion only supports tools.v0 function tools`);
5627
+ }
5628
+ if (!allowedToolSet.has(tool)) {
5629
+ throw new ConfigError(`unsupported tool "${tool}" (plugin conversion targets tools.v0)`);
5630
+ }
5631
+ mode = "client";
3954
5632
  }
3955
- if (!request.success_url?.trim()) {
3956
- throw new ConfigError("success_url is required");
5633
+ if (mode && mode !== "client") {
5634
+ throw new ConfigError(`tool_execution.mode must be "client" for plugin conversion`);
3957
5635
  }
3958
- if (!request.cancel_url?.trim()) {
3959
- throw new ConfigError("cancel_url is required");
5636
+ node.tool_execution = { mode: "client" };
5637
+ }
5638
+ }
5639
+ function parsePluginManifest(markdown) {
5640
+ const trimmed = markdown.trim();
5641
+ if (!trimmed) return {};
5642
+ const frontMatter = parseManifestFrontMatter(trimmed);
5643
+ if (frontMatter) return frontMatter;
5644
+ const lines = splitLines(trimmed);
5645
+ let name = "";
5646
+ for (const line of lines) {
5647
+ if (line.startsWith("# ")) {
5648
+ name = line.slice(2).trim();
5649
+ break;
3960
5650
  }
3961
- return await this.http.json(
3962
- `/tiers/${tierId}/checkout`,
3963
- {
3964
- method: "POST",
3965
- apiKey: this.apiKey,
3966
- body: request
5651
+ }
5652
+ let description = "";
5653
+ if (name) {
5654
+ let after = false;
5655
+ for (const line of lines) {
5656
+ const trimmedLine = line.trim();
5657
+ if (trimmedLine.startsWith("# ")) {
5658
+ after = true;
5659
+ continue;
5660
+ }
5661
+ if (!after) continue;
5662
+ if (!trimmedLine) continue;
5663
+ if (trimmedLine.startsWith("## ")) break;
5664
+ description = trimmedLine;
5665
+ break;
5666
+ }
5667
+ }
5668
+ return { name, description };
5669
+ }
5670
+ function parseManifestFrontMatter(markdown) {
5671
+ const lines = splitLines(markdown);
5672
+ if (!lines.length || lines[0].trim() !== "---") return null;
5673
+ const end = lines.findIndex((line, idx) => idx > 0 && line.trim() === "---");
5674
+ if (end === -1) return null;
5675
+ const manifest = {};
5676
+ let currentList = null;
5677
+ for (const line of lines.slice(1, end)) {
5678
+ const raw = line.trim();
5679
+ if (!raw || raw.startsWith("#")) continue;
5680
+ if (raw.startsWith("- ") && currentList) {
5681
+ const item = raw.slice(2).trim();
5682
+ if (item) {
5683
+ if (currentList === "commands") {
5684
+ manifest.commands = [...manifest.commands || [], asPluginCommandName(item)];
5685
+ } else {
5686
+ manifest.agents = [...manifest.agents || [], asPluginAgentName(item)];
5687
+ }
5688
+ }
5689
+ continue;
5690
+ }
5691
+ currentList = null;
5692
+ const [keyRaw, ...rest] = raw.split(":");
5693
+ if (!keyRaw || rest.length === 0) continue;
5694
+ const key = keyRaw.trim().toLowerCase();
5695
+ const val = rest.join(":").trim().replace(/^['"]|['"]$/g, "");
5696
+ if (key === "name") manifest.name = val;
5697
+ if (key === "description") manifest.description = val;
5698
+ if (key === "version") manifest.version = val;
5699
+ if (key === "commands") currentList = "commands";
5700
+ if (key === "agents") currentList = "agents";
5701
+ }
5702
+ manifest.commands = manifest.commands?.sort();
5703
+ manifest.agents = manifest.agents?.sort();
5704
+ return manifest;
5705
+ }
5706
+ function parseMarkdownFrontMatter(markdown) {
5707
+ const trimmed = markdown.trim();
5708
+ if (!trimmed.startsWith("---")) {
5709
+ return { body: markdown };
5710
+ }
5711
+ const lines = splitLines(trimmed);
5712
+ const endIdx = lines.findIndex((line, idx) => idx > 0 && line.trim() === "---");
5713
+ if (endIdx === -1) {
5714
+ return { body: markdown };
5715
+ }
5716
+ let description;
5717
+ let tools;
5718
+ let currentList = null;
5719
+ const toolItems = [];
5720
+ for (const line of lines.slice(1, endIdx)) {
5721
+ const raw = line.trim();
5722
+ if (!raw || raw.startsWith("#")) continue;
5723
+ if (raw.startsWith("- ") && currentList === "tools") {
5724
+ const item = raw.slice(2).trim();
5725
+ if (item) toolItems.push(item);
5726
+ continue;
5727
+ }
5728
+ currentList = null;
5729
+ const [keyRaw, ...rest] = raw.split(":");
5730
+ if (!keyRaw || rest.length === 0) continue;
5731
+ const key = keyRaw.trim().toLowerCase();
5732
+ const val = rest.join(":").trim().replace(/^['"]|['"]$/g, "");
5733
+ if (key === "description") description = val;
5734
+ if (key === "tools") {
5735
+ if (!val) {
5736
+ currentList = "tools";
5737
+ continue;
3967
5738
  }
5739
+ toolItems.push(...splitFrontMatterList(val));
5740
+ }
5741
+ }
5742
+ if (toolItems.length) {
5743
+ tools = toolItems.map((item) => parseToolName(item));
5744
+ }
5745
+ const body = lines.slice(endIdx + 1).join("\n").replace(/^[\n\r]+/, "");
5746
+ return { description, tools, body };
5747
+ }
5748
+ function parseToolName(raw) {
5749
+ const val = raw.trim();
5750
+ if (!allowedToolSet.has(val)) {
5751
+ throw new PluginOrchestrationError(
5752
+ PluginOrchestrationErrorCodes.UnknownTool,
5753
+ `unknown tool "${raw}"`
3968
5754
  );
3969
5755
  }
3970
- };
5756
+ return val;
5757
+ }
5758
+ function splitFrontMatterList(raw) {
5759
+ const cleaned = raw.trim().replace(/^\[/, "").replace(/\]$/, "");
5760
+ if (!cleaned) return [];
5761
+ return cleaned.split(",").map((part) => part.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
5762
+ }
5763
+ function extractAgentRefs(markdown) {
5764
+ const seen = /* @__PURE__ */ new Set();
5765
+ const out = [];
5766
+ const lines = splitLines(markdown);
5767
+ for (const line of lines) {
5768
+ const lower = line.toLowerCase();
5769
+ const idx = lower.indexOf("agents/");
5770
+ if (idx === -1) continue;
5771
+ if (!lower.includes(".md", idx)) continue;
5772
+ let seg = line.slice(idx).trim();
5773
+ seg = seg.replace(/^agents\//, "");
5774
+ seg = seg.split(".md")[0];
5775
+ seg = seg.replace(/[`* _]/g, "").trim();
5776
+ if (!seg || seen.has(seg)) continue;
5777
+ seen.add(seg);
5778
+ out.push(asPluginAgentName(seg));
5779
+ }
5780
+ return out.sort();
5781
+ }
5782
+ function splitLines(input) {
5783
+ return input.replace(/\r\n/g, "\n").split("\n");
5784
+ }
5785
+ function basename(path) {
5786
+ const parts = path.split("/");
5787
+ const last = parts[parts.length - 1] || "";
5788
+ return last.replace(/\.md$/i, "");
5789
+ }
5790
+ function joinRepoPath(base, elem) {
5791
+ const clean = (value) => value.replace(/^\/+|\/+$/g, "");
5792
+ const b = clean(base || "");
5793
+ const e = clean(elem || "");
5794
+ if (!b) return e;
5795
+ if (!e) return b;
5796
+ return `${b}/${e}`;
5797
+ }
5798
+ function sortedKeys(items) {
5799
+ return items.slice().sort();
5800
+ }
5801
+ function clonePlugin(plugin) {
5802
+ return {
5803
+ ...plugin,
5804
+ manifest: { ...plugin.manifest },
5805
+ commands: { ...plugin.commands },
5806
+ agents: { ...plugin.agents },
5807
+ rawFiles: { ...plugin.rawFiles },
5808
+ loadedAt: new Date(plugin.loadedAt)
5809
+ };
5810
+ }
5811
+ function asPluginId(value) {
5812
+ const trimmed = value.trim();
5813
+ if (!trimmed) throw new ConfigError("plugin id required");
5814
+ return trimmed;
5815
+ }
5816
+ function asPluginUrl(value) {
5817
+ const trimmed = value.trim();
5818
+ if (!trimmed) throw new ConfigError("plugin url required");
5819
+ return trimmed;
5820
+ }
5821
+ function asPluginCommandName(value) {
5822
+ const trimmed = value.trim();
5823
+ if (!trimmed) throw new ConfigError("plugin command name required");
5824
+ return trimmed;
5825
+ }
5826
+ function asPluginAgentName(value) {
5827
+ const trimmed = value.trim();
5828
+ if (!trimmed) throw new ConfigError("plugin agent name required");
5829
+ return trimmed;
5830
+ }
5831
+ function parseGitHubPluginRef(raw) {
5832
+ let url = raw.trim();
5833
+ if (!url) {
5834
+ throw new ConfigError("source url required");
5835
+ }
5836
+ if (url.startsWith("git@github.com:")) {
5837
+ url = `https://github.com/${url.replace("git@github.com:", "")}`;
5838
+ }
5839
+ if (!url.includes("://")) {
5840
+ url = `https://${url}`;
5841
+ }
5842
+ const parsed = new URL(url);
5843
+ const host = parsed.hostname.replace(/^www\./, "").toLowerCase();
5844
+ if (host !== "github.com" && host !== "raw.githubusercontent.com") {
5845
+ throw new ConfigError(`unsupported host: ${parsed.hostname}`);
5846
+ }
5847
+ let ref = parsed.searchParams.get("ref")?.trim() || "";
5848
+ const parts = parsed.pathname.split("/").filter(Boolean);
5849
+ if (parts.length < 2) {
5850
+ throw new ConfigError("invalid github url: expected /owner/repo");
5851
+ }
5852
+ const owner = parts[0];
5853
+ let repoPart = parts[1].replace(/\.git$/i, "");
5854
+ const atIdx = repoPart.indexOf("@");
5855
+ if (atIdx > 0 && atIdx < repoPart.length - 1) {
5856
+ if (!ref) {
5857
+ ref = repoPart.slice(atIdx + 1);
5858
+ }
5859
+ repoPart = repoPart.slice(0, atIdx);
5860
+ }
5861
+ const repo = repoPart;
5862
+ let rest = parts.slice(2);
5863
+ if (host === "github.com" && rest.length >= 2 && (rest[0] === "tree" || rest[0] === "blob")) {
5864
+ if (!ref) {
5865
+ ref = rest[1];
5866
+ }
5867
+ rest = rest.slice(2);
5868
+ }
5869
+ if (host === "raw.githubusercontent.com") {
5870
+ if (!rest.length) {
5871
+ throw new ConfigError("invalid raw github url");
5872
+ }
5873
+ if (!ref) {
5874
+ ref = rest[0];
5875
+ }
5876
+ rest = rest.slice(1);
5877
+ }
5878
+ let repoPath = rest.join("/");
5879
+ repoPath = repoPath.replace(/^\/+|\/+$/g, "");
5880
+ if (/plugin\.md$/i.test(repoPath) || /skill\.md$/i.test(repoPath)) {
5881
+ repoPath = repoPath.split("/").slice(0, -1).join("/");
5882
+ }
5883
+ if (/\.md$/i.test(repoPath)) {
5884
+ const commandsIdx = repoPath.indexOf("/commands/");
5885
+ if (commandsIdx >= 0) {
5886
+ repoPath = repoPath.slice(0, commandsIdx);
5887
+ }
5888
+ const agentsIdx = repoPath.indexOf("/agents/");
5889
+ if (agentsIdx >= 0) {
5890
+ repoPath = repoPath.slice(0, agentsIdx);
5891
+ }
5892
+ repoPath = repoPath.replace(/^\/+|\/+$/g, "");
5893
+ }
5894
+ if (!ref) {
5895
+ ref = DEFAULT_PLUGIN_REF;
5896
+ }
5897
+ const canonical = repoPath ? `github.com/${owner}/${repo}@${ref}/${repoPath}` : `github.com/${owner}/${repo}@${ref}`;
5898
+ return { owner, repo, ref, repoPath, canonical };
5899
+ }
5900
+ function specRequiresTools(spec) {
5901
+ for (const node of spec.nodes) {
5902
+ if (node.type === WorkflowNodeTypesIntent.LLM && node.tools?.length) {
5903
+ return true;
5904
+ }
5905
+ if (node.type === WorkflowNodeTypesIntent.MapFanout && node.subnode) {
5906
+ const sub = node.subnode;
5907
+ if (sub.tools?.length) {
5908
+ return true;
5909
+ }
5910
+ }
5911
+ }
5912
+ return false;
5913
+ }
3971
5914
 
3972
5915
  // src/http.ts
3973
5916
  var HTTPClient = class {
@@ -4357,21 +6300,176 @@ var CustomerResponsesClient = class {
4357
6300
  mergeCustomerOptions(this.customerId, options)
4358
6301
  );
4359
6302
  }
4360
- async streamTextDeltas(system, user, options = {}) {
4361
- return this.base.streamTextDeltasForCustomer(
4362
- this.customerId,
4363
- system,
4364
- user,
4365
- mergeCustomerOptions(this.customerId, options)
4366
- );
6303
+ async streamTextDeltas(system, user, options = {}) {
6304
+ return this.base.streamTextDeltasForCustomer(
6305
+ this.customerId,
6306
+ system,
6307
+ user,
6308
+ mergeCustomerOptions(this.customerId, options)
6309
+ );
6310
+ }
6311
+ };
6312
+ var CustomerScopedModelRelay = class {
6313
+ constructor(responses, customerId, baseUrl) {
6314
+ const normalized = normalizeCustomerId(customerId);
6315
+ this.responses = new CustomerResponsesClient(responses, normalized);
6316
+ this.customerId = normalized;
6317
+ this.baseUrl = baseUrl;
6318
+ }
6319
+ };
6320
+
6321
+ // src/tool_builder.ts
6322
+ function formatZodError(error) {
6323
+ if (error && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
6324
+ const issues = error.issues;
6325
+ return issues.map((issue) => {
6326
+ const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
6327
+ const msg = issue.message || "invalid";
6328
+ return path ? `${path}: ${msg}` : msg;
6329
+ }).join("; ");
6330
+ }
6331
+ return String(error);
6332
+ }
6333
+ var ToolBuilder = class {
6334
+ constructor() {
6335
+ this.entries = [];
6336
+ }
6337
+ /**
6338
+ * Add a tool with a Zod schema and handler.
6339
+ *
6340
+ * The handler receives parsed and validated arguments matching the schema.
6341
+ *
6342
+ * @param name - Tool name (must be unique)
6343
+ * @param description - Human-readable description of what the tool does
6344
+ * @param schema - Zod schema for the tool's parameters
6345
+ * @param handler - Function to execute when the tool is called
6346
+ * @returns this for chaining
6347
+ *
6348
+ * @example
6349
+ * ```typescript
6350
+ * tools.add(
6351
+ * "search_web",
6352
+ * "Search the web for information",
6353
+ * z.object({
6354
+ * query: z.string().describe("Search query"),
6355
+ * maxResults: z.number().optional().describe("Max results to return"),
6356
+ * }),
6357
+ * async (args) => {
6358
+ * // args is typed as { query: string; maxResults?: number }
6359
+ * return await searchAPI(args.query, args.maxResults);
6360
+ * }
6361
+ * );
6362
+ * ```
6363
+ */
6364
+ add(name, description, schema, handler) {
6365
+ const tool = createFunctionToolFromSchema(name, description, schema);
6366
+ this.entries.push({
6367
+ name,
6368
+ description,
6369
+ schema,
6370
+ handler,
6371
+ tool
6372
+ });
6373
+ return this;
6374
+ }
6375
+ /**
6376
+ * Get tool definitions for use with ResponseBuilder.tools().
6377
+ *
6378
+ * @example
6379
+ * ```typescript
6380
+ * const response = await mr.responses.create(
6381
+ * mr.responses.new()
6382
+ * .model("claude-sonnet-4-5")
6383
+ * .tools(tools.definitions())
6384
+ * .user("What's the weather in Paris?")
6385
+ * .build()
6386
+ * );
6387
+ * ```
6388
+ */
6389
+ definitions() {
6390
+ return this.entries.map((e) => e.tool);
6391
+ }
6392
+ /**
6393
+ * Get a ToolRegistry with all handlers registered.
6394
+ *
6395
+ * The handlers are wrapped to validate arguments against the schema
6396
+ * before invoking the user's handler. If validation fails, a
6397
+ * ToolArgsError is thrown (which ToolRegistry marks as retryable).
6398
+ *
6399
+ * Note: For mr.agent(), pass the ToolBuilder directly instead of calling
6400
+ * registry(). The agent method extracts both definitions and registry.
6401
+ *
6402
+ * @example
6403
+ * ```typescript
6404
+ * const registry = tools.registry();
6405
+ *
6406
+ * // Use with LocalSession (also pass definitions via defaultTools)
6407
+ * const session = mr.sessions.createLocal({
6408
+ * toolRegistry: registry,
6409
+ * defaultTools: tools.definitions(),
6410
+ * defaultModel: "claude-sonnet-4-5",
6411
+ * });
6412
+ * ```
6413
+ */
6414
+ registry() {
6415
+ const reg = new ToolRegistry();
6416
+ for (const entry of this.entries) {
6417
+ const validatingHandler = async (args, call) => {
6418
+ const result = entry.schema.safeParse(args);
6419
+ if (!result.success) {
6420
+ throw new ToolArgsError(
6421
+ `Invalid arguments for tool '${entry.name}': ${formatZodError(result.error)}`,
6422
+ call.id,
6423
+ entry.name,
6424
+ call.function?.arguments ?? ""
6425
+ );
6426
+ }
6427
+ return entry.handler(result.data, call);
6428
+ };
6429
+ reg.register(entry.name, validatingHandler);
6430
+ }
6431
+ return reg;
6432
+ }
6433
+ /**
6434
+ * Get both definitions and registry.
6435
+ *
6436
+ * Useful when you need both for manual tool handling.
6437
+ *
6438
+ * @example
6439
+ * ```typescript
6440
+ * const { definitions, registry } = tools.build();
6441
+ *
6442
+ * const response = await mr.responses.create(
6443
+ * mr.responses.new()
6444
+ * .model("claude-sonnet-4-5")
6445
+ * .tools(definitions)
6446
+ * .user("What's the weather?")
6447
+ * .build()
6448
+ * );
6449
+ *
6450
+ * if (hasToolCalls(response)) {
6451
+ * const results = await registry.executeAll(response.output[0].toolCalls!);
6452
+ * // ...
6453
+ * }
6454
+ * ```
6455
+ */
6456
+ build() {
6457
+ return {
6458
+ definitions: this.definitions(),
6459
+ registry: this.registry()
6460
+ };
6461
+ }
6462
+ /**
6463
+ * Get the number of tools defined.
6464
+ */
6465
+ get size() {
6466
+ return this.entries.length;
4367
6467
  }
4368
- };
4369
- var CustomerScopedModelRelay = class {
4370
- constructor(responses, customerId, baseUrl) {
4371
- const normalized = normalizeCustomerId(customerId);
4372
- this.responses = new CustomerResponsesClient(responses, normalized);
4373
- this.customerId = normalized;
4374
- this.baseUrl = baseUrl;
6468
+ /**
6469
+ * Check if a tool is defined.
6470
+ */
6471
+ has(name) {
6472
+ return this.entries.some((e) => e.name === name);
4375
6473
  }
4376
6474
  };
4377
6475
 
@@ -4550,6 +6648,12 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4550
6648
  model(model) {
4551
6649
  return this.with({ model: model.trim() });
4552
6650
  }
6651
+ maxParallelism(n) {
6652
+ return this.with({ maxParallelism: n });
6653
+ }
6654
+ inputs(decls) {
6655
+ return this.with({ inputs: decls });
6656
+ }
4553
6657
  node(node) {
4554
6658
  return this.with({ nodes: [...this.state.nodes, node] });
4555
6659
  }
@@ -4559,15 +6663,15 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4559
6663
  return this.node(configured.build());
4560
6664
  }
4561
6665
  joinAll(id) {
4562
- return this.node({ id, type: WorkflowNodeTypesLite.JoinAll });
6666
+ return this.node({ id, type: WorkflowNodeTypesIntent.JoinAll });
4563
6667
  }
4564
6668
  joinAny(id, predicate) {
4565
- return this.node({ id, type: WorkflowNodeTypesLite.JoinAny, predicate });
6669
+ return this.node({ id, type: WorkflowNodeTypesIntent.JoinAny, predicate });
4566
6670
  }
4567
6671
  joinCollect(id, options) {
4568
6672
  return this.node({
4569
6673
  id,
4570
- type: WorkflowNodeTypesLite.JoinCollect,
6674
+ type: WorkflowNodeTypesIntent.JoinCollect,
4571
6675
  limit: options.limit,
4572
6676
  timeout_ms: options.timeoutMs,
4573
6677
  predicate: options.predicate
@@ -4576,7 +6680,7 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4576
6680
  transformJSON(id, object, merge) {
4577
6681
  return this.node({
4578
6682
  id,
4579
- type: WorkflowNodeTypesLite.TransformJSON,
6683
+ type: WorkflowNodeTypesIntent.TransformJSON,
4580
6684
  object,
4581
6685
  merge
4582
6686
  });
@@ -4584,7 +6688,7 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4584
6688
  mapFanout(id, options) {
4585
6689
  return this.node({
4586
6690
  id,
4587
- type: WorkflowNodeTypesLite.MapFanout,
6691
+ type: WorkflowNodeTypesIntent.MapFanout,
4588
6692
  items_from: options.itemsFrom,
4589
6693
  items_from_input: options.itemsFromInput,
4590
6694
  items_path: options.itemsPath,
@@ -4605,7 +6709,14 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4605
6709
  ...node,
4606
6710
  depends_on: node.depends_on ? [...node.depends_on] : void 0
4607
6711
  }));
4608
- const byId = new Map(nodes.map((node, idx) => [node.id, idx]));
6712
+ const byId = /* @__PURE__ */ new Map();
6713
+ for (let idx = 0; idx < nodes.length; idx++) {
6714
+ const id = nodes[idx].id;
6715
+ if (byId.has(id)) {
6716
+ throw new Error(`duplicate node id "${id}"`);
6717
+ }
6718
+ byId.set(id, idx);
6719
+ }
4609
6720
  for (const edge of this.state.edges) {
4610
6721
  const idx = byId.get(edge.to);
4611
6722
  if (idx === void 0) {
@@ -4621,6 +6732,8 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4621
6732
  kind: WorkflowKinds.WorkflowIntent,
4622
6733
  name: this.state.name,
4623
6734
  model: this.state.model,
6735
+ max_parallelism: this.state.maxParallelism,
6736
+ inputs: this.state.inputs,
4624
6737
  nodes,
4625
6738
  outputs: [...this.state.outputs]
4626
6739
  };
@@ -4628,7 +6741,7 @@ var WorkflowIntentBuilder = class _WorkflowIntentBuilder {
4628
6741
  };
4629
6742
  var LLMNodeBuilder = class {
4630
6743
  constructor(id) {
4631
- this.node = { id, type: WorkflowNodeTypesLite.LLM };
6744
+ this.node = { id, type: WorkflowNodeTypesIntent.LLM };
4632
6745
  }
4633
6746
  system(text) {
4634
6747
  this.node.system = text;
@@ -4665,6 +6778,45 @@ var LLMNodeBuilder = class {
4665
6778
  function workflowIntent() {
4666
6779
  return new WorkflowIntentBuilder();
4667
6780
  }
6781
+ function llm(id, configure) {
6782
+ const builder = new LLMNodeBuilder(parseNodeId(id));
6783
+ const configured = configure ? configure(builder) : builder;
6784
+ return configured.build();
6785
+ }
6786
+ function chain(steps, options) {
6787
+ let builder = new WorkflowIntentBuilder();
6788
+ if (options?.name) {
6789
+ builder = builder.name(options.name);
6790
+ }
6791
+ if (options?.model) {
6792
+ builder = builder.model(options.model);
6793
+ }
6794
+ for (const step of steps) {
6795
+ builder = builder.node(step);
6796
+ }
6797
+ for (let i = 1; i < steps.length; i++) {
6798
+ builder = builder.edge(steps[i - 1].id, steps[i].id);
6799
+ }
6800
+ return builder;
6801
+ }
6802
+ function parallel(steps, options) {
6803
+ let builder = new WorkflowIntentBuilder();
6804
+ const joinId = parseNodeId(options?.joinId ?? "join");
6805
+ if (options?.name) {
6806
+ builder = builder.name(options.name);
6807
+ }
6808
+ if (options?.model) {
6809
+ builder = builder.model(options.model);
6810
+ }
6811
+ for (const step of steps) {
6812
+ builder = builder.node(step);
6813
+ }
6814
+ builder = builder.joinAll(joinId);
6815
+ for (const step of steps) {
6816
+ builder = builder.edge(step.id, joinId);
6817
+ }
6818
+ return builder;
6819
+ }
4668
6820
 
4669
6821
  // src/testing.ts
4670
6822
  function buildNDJSONResponse(lines, headers = {}, status = 200) {
@@ -4732,154 +6884,6 @@ function createMockFetchQueue(responses) {
4732
6884
  return { fetch: fetchImpl, calls };
4733
6885
  }
4734
6886
 
4735
- // src/tools_runner.ts
4736
- var ToolRunner = class {
4737
- constructor(options) {
4738
- this.registry = options.registry;
4739
- this.runsClient = options.runsClient;
4740
- this.customerId = options.customerId;
4741
- this.onBeforeExecute = options.onBeforeExecute;
4742
- this.onAfterExecute = options.onAfterExecute;
4743
- this.onSubmitted = options.onSubmitted;
4744
- this.onError = options.onError;
4745
- }
4746
- /**
4747
- * Handles a node_waiting event by executing tools and submitting results.
4748
- *
4749
- * @param runId - The run ID
4750
- * @param nodeId - The node ID that is waiting
4751
- * @param waiting - The waiting state with pending tool calls
4752
- * @returns The submission response with accepted count and new status
4753
- *
4754
- * @example
4755
- * ```typescript
4756
- * for await (const event of client.runs.events(runId)) {
4757
- * if (event.type === "node_waiting") {
4758
- * const result = await runner.handleNodeWaiting(
4759
- * runId,
4760
- * event.node_id,
4761
- * event.waiting
4762
- * );
4763
- * console.log(`Submitted ${result.accepted} results, status: ${result.status}`);
4764
- * }
4765
- * }
4766
- * ```
4767
- */
4768
- async handleNodeWaiting(runId, nodeId, waiting) {
4769
- const results = [];
4770
- for (const pending of waiting.pending_tool_calls) {
4771
- try {
4772
- await this.onBeforeExecute?.(pending);
4773
- const toolCall = createToolCall(
4774
- pending.tool_call.id,
4775
- pending.tool_call.name,
4776
- pending.tool_call.arguments
4777
- );
4778
- const result = await this.registry.execute(toolCall);
4779
- results.push(result);
4780
- await this.onAfterExecute?.(result);
4781
- } catch (err) {
4782
- const error = err instanceof Error ? err : new Error(String(err));
4783
- await this.onError?.(error, pending);
4784
- results.push({
4785
- toolCallId: pending.tool_call.id,
4786
- toolName: pending.tool_call.name,
4787
- result: null,
4788
- error: error.message
4789
- });
4790
- }
4791
- }
4792
- const response = await this.runsClient.submitToolResults(
4793
- runId,
4794
- {
4795
- node_id: nodeId,
4796
- step: waiting.step,
4797
- request_id: waiting.request_id,
4798
- results: results.map((r) => ({
4799
- tool_call: {
4800
- id: r.toolCallId,
4801
- name: r.toolName
4802
- },
4803
- output: r.error ? `Error: ${r.error}` : typeof r.result === "string" ? r.result : JSON.stringify(r.result)
4804
- }))
4805
- },
4806
- { customerId: this.customerId }
4807
- );
4808
- await this.onSubmitted?.(runId, response.accepted, response.status);
4809
- return {
4810
- accepted: response.accepted,
4811
- status: response.status,
4812
- results
4813
- };
4814
- }
4815
- /**
4816
- * Processes a stream of run events, automatically handling node_waiting events.
4817
- *
4818
- * This is the main entry point for running a workflow with client-side tools.
4819
- * It yields all events through (including node_waiting after handling).
4820
- *
4821
- * @param runId - The run ID to process
4822
- * @param events - AsyncIterable of run events (from RunsClient.events())
4823
- * @yields All run events, with node_waiting events handled automatically
4824
- *
4825
- * @example
4826
- * ```typescript
4827
- * const run = await client.runs.create(workflowSpec);
4828
- * const eventStream = client.runs.events(run.run_id);
4829
- *
4830
- * for await (const event of runner.processEvents(run.run_id, eventStream)) {
4831
- * switch (event.type) {
4832
- * case "node_started":
4833
- * console.log(`Node ${event.node_id} started`);
4834
- * break;
4835
- * case "node_succeeded":
4836
- * console.log(`Node ${event.node_id} succeeded`);
4837
- * break;
4838
- * case "run_succeeded":
4839
- * console.log("Run completed!");
4840
- * break;
4841
- * }
4842
- * }
4843
- * ```
4844
- */
4845
- async *processEvents(runId, events) {
4846
- for await (const event of events) {
4847
- if (event.type === "node_waiting") {
4848
- const waitingEvent = event;
4849
- try {
4850
- await this.handleNodeWaiting(
4851
- runId,
4852
- waitingEvent.node_id,
4853
- waitingEvent.waiting
4854
- );
4855
- } catch (err) {
4856
- const error = err instanceof Error ? err : new Error(String(err));
4857
- await this.onError?.(error);
4858
- throw error;
4859
- }
4860
- }
4861
- yield event;
4862
- }
4863
- }
4864
- /**
4865
- * Checks if a run event is a node_waiting event.
4866
- * Utility for filtering events when not using processEvents().
4867
- */
4868
- static isNodeWaiting(event) {
4869
- return event.type === "node_waiting";
4870
- }
4871
- /**
4872
- * Checks if a run status is terminal (succeeded, failed, or canceled).
4873
- * Utility for determining when to stop polling.
4874
- */
4875
- static isTerminalStatus(status) {
4876
- return status === "succeeded" || status === "failed" || status === "canceled";
4877
- }
4878
- };
4879
- function createToolRunner(options) {
4880
- return new ToolRunner(options);
4881
- }
4882
-
4883
6887
  // src/generated/index.ts
4884
6888
  var generated_exports = {};
4885
6889
 
@@ -4890,7 +6894,7 @@ __export(workflow_exports, {
4890
6894
  LLMNodeBuilder: () => LLMNodeBuilder,
4891
6895
  LLM_TEXT_OUTPUT: () => LLM_TEXT_OUTPUT,
4892
6896
  LLM_USER_MESSAGE_TEXT: () => LLM_USER_MESSAGE_TEXT,
4893
- NodeTypesLite: () => NodeTypesLite,
6897
+ NodeTypesIntent: () => NodeTypesIntent,
4894
6898
  WorkflowIntentBuilder: () => WorkflowIntentBuilder,
4895
6899
  parseNodeId: () => parseNodeId,
4896
6900
  parseOutputName: () => parseOutputName,
@@ -4899,17 +6903,17 @@ __export(workflow_exports, {
4899
6903
  workflowIntent: () => workflowIntent
4900
6904
  });
4901
6905
  var KindIntent = WorkflowKinds.WorkflowIntent;
4902
- var NodeTypesLite = {
4903
- LLM: WorkflowNodeTypesLite.LLM,
4904
- JoinAll: WorkflowNodeTypesLite.JoinAll,
4905
- JoinAny: WorkflowNodeTypesLite.JoinAny,
4906
- JoinCollect: WorkflowNodeTypesLite.JoinCollect,
4907
- TransformJSON: WorkflowNodeTypesLite.TransformJSON,
4908
- MapFanout: WorkflowNodeTypesLite.MapFanout
6906
+ var NodeTypesIntent = {
6907
+ LLM: WorkflowNodeTypesIntent.LLM,
6908
+ JoinAll: WorkflowNodeTypesIntent.JoinAll,
6909
+ JoinAny: WorkflowNodeTypesIntent.JoinAny,
6910
+ JoinCollect: WorkflowNodeTypesIntent.JoinCollect,
6911
+ TransformJSON: WorkflowNodeTypesIntent.TransformJSON,
6912
+ MapFanout: WorkflowNodeTypesIntent.MapFanout
4909
6913
  };
4910
6914
 
4911
6915
  // src/index.ts
4912
- var ModelRelay = class _ModelRelay {
6916
+ var _ModelRelay = class _ModelRelay {
4913
6917
  static fromSecretKey(secretKey, options = {}) {
4914
6918
  return new _ModelRelay({ ...options, key: parseSecretKey(secretKey) });
4915
6919
  }
@@ -4958,22 +6962,187 @@ var ModelRelay = class _ModelRelay {
4958
6962
  });
4959
6963
  this.images = new ImagesClient(this.http, auth);
4960
6964
  this.sessions = new SessionsClient(this, this.http, auth);
6965
+ this.stateHandles = new StateHandlesClient(this.http, auth);
4961
6966
  this.tiers = new TiersClient(this.http, { apiKey, accessToken });
6967
+ this.plugins = new PluginsClient({
6968
+ responses: this.responses,
6969
+ http: this.http,
6970
+ auth,
6971
+ runs: this.runs
6972
+ });
4962
6973
  }
4963
6974
  forCustomer(customerId) {
4964
6975
  return new CustomerScopedModelRelay(this.responses, customerId, this.baseUrl);
4965
6976
  }
6977
+ /**
6978
+ * Simple chat completion with system and user prompt.
6979
+ *
6980
+ * Returns the full Response object for access to usage, model, etc.
6981
+ *
6982
+ * @example
6983
+ * ```typescript
6984
+ * const response = await mr.chat("claude-sonnet-4-5", "Hello!");
6985
+ * console.log(response.output);
6986
+ * console.log(response.usage);
6987
+ * ```
6988
+ *
6989
+ * @example With system prompt
6990
+ * ```typescript
6991
+ * const response = await mr.chat("claude-sonnet-4-5", "Explain quantum computing", {
6992
+ * system: "You are a physics professor",
6993
+ * });
6994
+ * ```
6995
+ */
6996
+ async chat(model, prompt, options = {}) {
6997
+ const { system, customerId, ...reqOptions } = options;
6998
+ let builder = this.responses.new().model(asModelId(model));
6999
+ if (system) {
7000
+ builder = builder.system(system);
7001
+ }
7002
+ builder = builder.user(prompt);
7003
+ if (customerId) {
7004
+ builder = builder.customerId(customerId);
7005
+ }
7006
+ return this.responses.create(builder.build(), reqOptions);
7007
+ }
7008
+ /**
7009
+ * Simple prompt that returns just the text response.
7010
+ *
7011
+ * The most ergonomic way to get a quick answer.
7012
+ *
7013
+ * @example
7014
+ * ```typescript
7015
+ * const answer = await mr.ask("claude-sonnet-4-5", "What is 2 + 2?");
7016
+ * console.log(answer); // "4"
7017
+ * ```
7018
+ *
7019
+ * @example With system prompt
7020
+ * ```typescript
7021
+ * const haiku = await mr.ask("claude-sonnet-4-5", "Write about the ocean", {
7022
+ * system: "You are a poet who only writes haikus",
7023
+ * });
7024
+ * ```
7025
+ */
7026
+ async ask(model, prompt, options = {}) {
7027
+ const response = await this.chat(model, prompt, options);
7028
+ return extractAssistantText(response.output);
7029
+ }
7030
+ /**
7031
+ * Run an agentic tool loop to completion.
7032
+ *
7033
+ * Runs API calls in a loop until the model stops calling tools
7034
+ * or maxTurns is reached.
7035
+ *
7036
+ * @example
7037
+ * ```typescript
7038
+ * import { z } from "zod";
7039
+ *
7040
+ * const tools = mr.tools()
7041
+ * .add("read_file", "Read a file", z.object({ path: z.string() }), async (args) => {
7042
+ * return fs.readFile(args.path, "utf-8");
7043
+ * })
7044
+ * .add("write_file", "Write a file", z.object({ path: z.string(), content: z.string() }), async (args) => {
7045
+ * await fs.writeFile(args.path, args.content);
7046
+ * return "File written successfully";
7047
+ * });
7048
+ *
7049
+ * const result = await mr.agent("claude-sonnet-4-5", {
7050
+ * tools,
7051
+ * prompt: "Read config.json and add a version field",
7052
+ * });
7053
+ *
7054
+ * console.log(result.output); // Final text response
7055
+ * console.log(result.usage); // Total tokens used
7056
+ * ```
7057
+ *
7058
+ * @example With system prompt and maxTurns
7059
+ * ```typescript
7060
+ * const result = await mr.agent("claude-sonnet-4-5", {
7061
+ * tools,
7062
+ * prompt: "Refactor the auth module",
7063
+ * system: "You are a senior TypeScript developer",
7064
+ * maxTurns: 50, // or ModelRelay.NO_TURN_LIMIT for unlimited
7065
+ * });
7066
+ * ```
7067
+ */
7068
+ async agent(model, options) {
7069
+ const { definitions, registry } = options.tools.build();
7070
+ const maxTurns = options.maxTurns ?? _ModelRelay.DEFAULT_MAX_TURNS;
7071
+ const modelId = asModelId(model);
7072
+ const input = [];
7073
+ if (options.system) {
7074
+ input.push(createSystemMessage(options.system));
7075
+ }
7076
+ input.push(createUserMessage(options.prompt));
7077
+ const outcome = await runToolLoop({
7078
+ client: this.responses,
7079
+ input,
7080
+ tools: definitions,
7081
+ registry,
7082
+ maxTurns,
7083
+ buildRequest: (builder) => builder.model(modelId)
7084
+ });
7085
+ if (outcome.status !== "complete") {
7086
+ throw new ConfigError("agent tool loop requires a tool registry");
7087
+ }
7088
+ return {
7089
+ output: outcome.output,
7090
+ usage: outcome.usage,
7091
+ response: outcome.response
7092
+ };
7093
+ }
7094
+ /**
7095
+ * Creates a fluent tool builder for defining tools with Zod schemas.
7096
+ *
7097
+ * @example
7098
+ * ```typescript
7099
+ * import { z } from "zod";
7100
+ *
7101
+ * const tools = mr.tools()
7102
+ * .add("get_weather", "Get current weather", z.object({ location: z.string() }), async (args) => {
7103
+ * return { temp: 72, unit: "fahrenheit" };
7104
+ * })
7105
+ * .add("read_file", "Read a file", z.object({ path: z.string() }), async (args) => {
7106
+ * return fs.readFile(args.path, "utf-8");
7107
+ * });
7108
+ *
7109
+ * // Use with agent (pass ToolBuilder directly)
7110
+ * const result = await mr.agent("claude-sonnet-4-5", {
7111
+ * tools,
7112
+ * prompt: "What's the weather in Paris?",
7113
+ * });
7114
+ *
7115
+ * // Or get tool definitions for manual use
7116
+ * const toolDefs = tools.definitions();
7117
+ * ```
7118
+ */
7119
+ tools() {
7120
+ return new ToolBuilder();
7121
+ }
4966
7122
  };
7123
+ // =========================================================================
7124
+ // Convenience Methods (Simple Case Simple)
7125
+ // =========================================================================
7126
+ /** Default maximum turns for agent loops. */
7127
+ _ModelRelay.DEFAULT_MAX_TURNS = 100;
7128
+ /**
7129
+ * Use this for maxTurns to disable the turn limit.
7130
+ * Use with caution as this can lead to infinite loops and runaway API costs.
7131
+ */
7132
+ _ModelRelay.NO_TURN_LIMIT = Number.MAX_SAFE_INTEGER;
7133
+ var ModelRelay = _ModelRelay;
4967
7134
  function resolveBaseUrl(override) {
4968
7135
  const base = override || DEFAULT_BASE_URL;
4969
7136
  return base.replace(/\/+$/, "");
4970
7137
  }
4971
7138
  export {
4972
7139
  APIError,
7140
+ AgentMaxTurnsError,
4973
7141
  AuthClient,
4974
7142
  BillingProviders,
4975
7143
  ConfigError,
4976
7144
  ContentPartTypes,
7145
+ ContextManager,
4977
7146
  CustomerResponsesClient,
4978
7147
  CustomerScopedModelRelay,
4979
7148
  CustomerTokenProvider,
@@ -4982,6 +7151,7 @@ export {
4982
7151
  DEFAULT_CONNECT_TIMEOUT_MS,
4983
7152
  DEFAULT_REQUEST_TIMEOUT_MS,
4984
7153
  ErrorCodes,
7154
+ FileConversationStore,
4985
7155
  ImagesClient,
4986
7156
  InputItemTypes,
4987
7157
  JoinOutput,
@@ -5002,19 +7172,29 @@ export {
5002
7172
  LLM_TEXT_OUTPUT,
5003
7173
  LLM_USER_MESSAGE_TEXT,
5004
7174
  LocalSession,
5005
- MemorySessionStore,
7175
+ MAX_STATE_HANDLE_TTL_SECONDS,
7176
+ MemoryConversationStore,
5006
7177
  MessageRoles,
5007
7178
  ModelRelay,
5008
7179
  ModelRelayError,
7180
+ OrchestrationModes,
5009
7181
  OutputFormatTypes,
5010
7182
  OutputItemTypes,
5011
7183
  PathEscapeError,
7184
+ PluginConverter,
7185
+ PluginLoader,
7186
+ PluginOrchestrationError,
7187
+ PluginOrchestrationErrorCodes,
7188
+ PluginRunner,
7189
+ PluginToolNames,
7190
+ PluginsClient,
5012
7191
  ResponsesClient,
5013
7192
  ResponsesStream,
5014
7193
  RunsClient,
5015
7194
  RunsEventStream,
5016
7195
  SDK_VERSION,
5017
7196
  SessionsClient,
7197
+ SqliteConversationStore,
5018
7198
  StopReasons,
5019
7199
  StreamProtocolError,
5020
7200
  StreamTimeoutError,
@@ -5025,18 +7205,19 @@ export {
5025
7205
  TiersClient,
5026
7206
  ToolArgsError,
5027
7207
  ToolArgumentError,
7208
+ ToolBuilder,
5028
7209
  ToolCallAccumulator,
5029
7210
  ToolChoiceTypes,
5030
7211
  ToolRegistry,
5031
7212
  ToolRunner,
5032
7213
  ToolTypes,
5033
7214
  TransportError,
7215
+ USER_ASK_TOOL_NAME,
5034
7216
  WORKFLOWS_COMPILE_PATH,
5035
- WebToolIntents,
5036
7217
  WorkflowIntentBuilder,
5037
7218
  WorkflowKinds,
5038
- WorkflowNodeTypesLite as WorkflowNodeTypes,
5039
- WorkflowNodeTypesLite,
7219
+ WorkflowNodeTypesIntent as WorkflowNodeTypes,
7220
+ WorkflowNodeTypesIntent,
5040
7221
  WorkflowValidationError,
5041
7222
  WorkflowsClient,
5042
7223
  asModelId,
@@ -5046,22 +7227,26 @@ export {
5046
7227
  assistantMessageWithToolCalls,
5047
7228
  buildDelayedNDJSONResponse,
5048
7229
  buildNDJSONResponse,
7230
+ chain,
5049
7231
  createAccessTokenAuth,
5050
7232
  createApiKeyAuth,
5051
7233
  createAssistantMessage,
7234
+ createFileConversationStore,
5052
7235
  createFunctionCall,
5053
7236
  createFunctionTool,
5054
- createFunctionToolFromSchema,
5055
7237
  createLocalSession,
5056
- createMemorySessionStore,
7238
+ createMemoryConversationStore,
5057
7239
  createMockFetchQueue,
7240
+ createModelContextResolver,
5058
7241
  createRetryMessages,
7242
+ createSqliteConversationStore,
5059
7243
  createSystemMessage,
5060
7244
  createToolCall,
5061
7245
  createToolRunner,
7246
+ createTypedTool,
5062
7247
  createUsage,
7248
+ createUserAskTool,
5063
7249
  createUserMessage,
5064
- createWebTool,
5065
7250
  defaultRetryHandler,
5066
7251
  defaultTierModelId,
5067
7252
  executeWithRetry,
@@ -5069,16 +7254,26 @@ export {
5069
7254
  formatToolErrorForModel,
5070
7255
  generateSessionId,
5071
7256
  generated_exports as generated,
7257
+ getAllToolCalls,
7258
+ getAssistantText,
5072
7259
  getRetryableErrors,
7260
+ getToolArgs,
7261
+ getToolArgsRaw,
7262
+ getToolName,
7263
+ getTypedToolCall,
7264
+ getTypedToolCalls,
5073
7265
  hasRetryableErrors,
5074
7266
  hasToolCalls,
5075
7267
  isSecretKey,
7268
+ isUserAskToolCall,
7269
+ llm,
5076
7270
  mergeMetrics,
5077
7271
  mergeTrace,
5078
7272
  modelToString,
5079
7273
  normalizeModelId,
5080
7274
  normalizeStopReason,
5081
7275
  outputFormatFromZod,
7276
+ parallel,
5082
7277
  parseApiKey,
5083
7278
  parseErrorResponse,
5084
7279
  parseNodeId,
@@ -5086,15 +7281,20 @@ export {
5086
7281
  parsePlanHash,
5087
7282
  parseRunId,
5088
7283
  parseSecretKey,
5089
- parseToolArgs,
5090
- parseToolArgsRaw,
7284
+ parseTypedToolCall,
7285
+ parseUserAskArgs,
7286
+ prepareInputWithContext,
5091
7287
  respondToToolCall,
7288
+ runToolLoop,
7289
+ serializeUserAskResult,
5092
7290
  stopReasonToString,
5093
7291
  toolChoiceAuto,
5094
7292
  toolChoiceNone,
5095
7293
  toolChoiceRequired,
5096
7294
  toolResultMessage,
5097
- tryParseToolArgs,
7295
+ truncateInputByTokens,
7296
+ userAskResultChoice,
7297
+ userAskResultFreeform,
5098
7298
  validateWithZod,
5099
7299
  workflow_exports as workflow,
5100
7300
  workflowIntent,