@botcord/daemon 0.2.76 → 0.2.78

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/src/daemon.ts CHANGED
@@ -684,7 +684,7 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
684
684
  */
685
685
  export interface BootBackfillResult {
686
686
  credentialPathByAgentId: Map<string, string>;
687
- agentRuntimes: Record<string, { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }>;
687
+ agentRuntimes: Record<string, { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }>;
688
688
  }
689
689
 
690
690
  /**
@@ -703,13 +703,25 @@ export function backfillBootAgents(
703
703
  ): BootBackfillResult {
704
704
  const ensure = opts.ensure ?? ensureAgentWorkspace;
705
705
  const credentialPathByAgentId = new Map<string, string>();
706
- const agentRuntimes: Record<string, { runtime?: string; cwd?: string }> = {};
706
+ const agentRuntimes: Record<string, { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }> = {};
707
707
  const failed: string[] = [];
708
708
  for (const a of agents) {
709
709
  if (a.credentialsFile) credentialPathByAgentId.set(a.agentId, a.credentialsFile);
710
- if (a.runtime || a.cwd || a.openclawGateway || a.openclawAgent || a.hermesProfile) {
710
+ if (
711
+ a.runtime ||
712
+ a.runtimeModel ||
713
+ a.reasoningEffort ||
714
+ typeof a.thinking === "boolean" ||
715
+ a.cwd ||
716
+ a.openclawGateway ||
717
+ a.openclawAgent ||
718
+ a.hermesProfile
719
+ ) {
711
720
  agentRuntimes[a.agentId] = {
712
721
  ...(a.runtime ? { runtime: a.runtime } : {}),
722
+ ...(a.runtimeModel ? { runtimeModel: a.runtimeModel } : {}),
723
+ ...(a.reasoningEffort ? { reasoningEffort: a.reasoningEffort } : {}),
724
+ ...(typeof a.thinking === "boolean" ? { thinking: a.thinking } : {}),
713
725
  ...(a.cwd ? { cwd: a.cwd } : {}),
714
726
  ...(a.openclawGateway ? { openclawGateway: a.openclawGateway } : {}),
715
727
  ...(a.openclawAgent ? { openclawAgent: a.openclawAgent } : {}),
@@ -769,6 +769,30 @@ describe("createBotCordChannel — streamBlock()", () => {
769
769
  });
770
770
  });
771
771
 
772
+ it("normalizes DeepSeek tool input so the dashboard can expand it", () => {
773
+ expect(
774
+ __normalizeBlockForHubForTests(
775
+ {
776
+ kind: "tool_use",
777
+ seq: 4,
778
+ raw: {
779
+ event: "tool.started",
780
+ payload: { id: "tool_1", name: "exec_shell", input: { cmd: "pwd" } },
781
+ },
782
+ },
783
+ 4,
784
+ ),
785
+ ).toEqual({
786
+ kind: "tool_call",
787
+ seq: 4,
788
+ payload: {
789
+ id: "tool_1",
790
+ name: "exec_shell",
791
+ params: { cmd: "pwd" },
792
+ },
793
+ });
794
+ });
795
+
772
796
  it("POSTs to /hub/stream-block with the right trace_id + block", async () => {
773
797
  const fetchSpy = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
774
798
  const realFetch = globalThis.fetch;
@@ -106,7 +106,12 @@ async function startMockDeepseekServer(opts?: {
106
106
  };
107
107
  }
108
108
 
109
- function runAdapter(serverUrl: string, authToken: string, sessionId: string | null = null) {
109
+ function runAdapter(
110
+ serverUrl: string,
111
+ authToken: string,
112
+ sessionId: string | null = null,
113
+ extraArgs?: string[],
114
+ ) {
110
115
  const adapter = new DeepseekTuiAdapter({ serverUrl, authToken });
111
116
  const ctrl = new AbortController();
112
117
  const blocks: string[] = [];
@@ -118,6 +123,7 @@ function runAdapter(serverUrl: string, authToken: string, sessionId: string | nu
118
123
  cwd: tmpRoot,
119
124
  signal: ctrl.signal,
120
125
  trustLevel: "owner",
126
+ extraArgs,
121
127
  systemContext: "runtime memory",
122
128
  onBlock: (b) => blocks.push(b.kind),
123
129
  onStatus: (e) => {
@@ -184,6 +190,29 @@ describe("DeepseekTuiAdapter", () => {
184
190
  }
185
191
  });
186
192
 
193
+ it("passes selected model and reasoning effort through HTTP payloads", async () => {
194
+ const server = await startMockDeepseekServer();
195
+ try {
196
+ const { result } = runAdapter(server.baseUrl, server.token, null, [
197
+ "--model",
198
+ "deepseek-v4-pro",
199
+ "--reasoning-effort",
200
+ "auto",
201
+ ]);
202
+ await result;
203
+ expect(server.calls.find((c) => c.method === "POST" && c.url === "/v1/threads")?.body).toMatchObject({
204
+ model: "deepseek-v4-pro",
205
+ reasoning_effort: "auto",
206
+ });
207
+ expect(server.calls.find((c) => c.method === "POST" && c.url.endsWith("/turns"))?.body).toMatchObject({
208
+ model: "deepseek-v4-pro",
209
+ reasoning_effort: "auto",
210
+ });
211
+ } finally {
212
+ await server.close();
213
+ }
214
+ });
215
+
187
216
  it("clears stale session ids when DeepSeek reports the thread missing", async () => {
188
217
  const server = await startMockDeepseekServer({ threadId: "thr_other" });
189
218
  try {
@@ -153,10 +153,11 @@ function defaultClientFactory(input: {
153
153
  */
154
154
  function isOwnerTrust(msg: InboxMessage): boolean {
155
155
  if (msg.room_id?.startsWith(OWNER_CHAT_PREFIX)) return true;
156
- if (msg.source_type === "dashboard_user_chat") return true;
156
+ const sourceType = msg.source_type as string | undefined;
157
+ if (sourceType === "dashboard_user_chat") return true;
157
158
  // Cloud Agent run tasks are Hub-issued on the user's behalf, same
158
159
  // trust posture as owner chat.
159
- if (msg.source_type === "cloud_agent_run") return true;
160
+ if (sourceType === "cloud_agent_run") return true;
160
161
  return false;
161
162
  }
162
163
 
@@ -197,10 +198,11 @@ function normalizeInbox(
197
198
  // run completes). All other envelope types (notification, system,
198
199
  // contact_added/removed, …) are still filtered out — they belong in
199
200
  // a separate push-notification path that daemon does not yet implement.
201
+ const envType = env.type as string;
200
202
  if (
201
- env.type !== "message" &&
202
- env.type !== "contact_request" &&
203
- env.type !== "cloud_run"
203
+ envType !== "message" &&
204
+ envType !== "contact_request" &&
205
+ envType !== "cloud_run"
204
206
  )
205
207
  return null;
206
208
  if (!msg.room_id) return null;
@@ -214,8 +216,9 @@ function normalizeInbox(
214
216
 
215
217
  const isDm = msg.room_id.startsWith(DM_ROOM_PREFIX);
216
218
  const isOwnerChat = msg.room_id.startsWith(OWNER_CHAT_PREFIX);
219
+ const sourceType = msg.source_type as string | undefined;
217
220
  const senderKind: "user" | "agent" =
218
- ownerTrust || msg.source_type === "dashboard_human_room" ? "user" : "agent";
221
+ ownerTrust || sourceType === "dashboard_human_room" ? "user" : "agent";
219
222
 
220
223
  const senderName = msg.source_user_name ?? undefined;
221
224
  const threadId = msg.topic_id ?? msg.topic ?? null;
@@ -1005,45 +1008,25 @@ function normalizeBlockForHub(
1005
1008
  }
1006
1009
 
1007
1010
  if (kind === "tool_use") {
1008
- // Claude Code: assistant message w/ content[].type === "tool_use" {id,name,input}
1009
- // Codex: item.started for command_execution, file_change, mcp_tool_call, web_search
1010
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1011
- const tu = contents.find((c: any) => c?.type === "tool_use");
1012
- if (tu) {
1013
- payload.name = typeof tu.name === "string" ? tu.name : "tool";
1014
- if (tu.input && typeof tu.input === "object") payload.params = tu.input;
1015
- if (typeof tu.id === "string") payload.id = tu.id;
1016
- } else if (raw?.item && typeof raw.item === "object") {
1017
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
1018
- const params = codexToolParams(raw.item);
1019
- if (Object.keys(params).length > 0) payload.params = params;
1020
- if (typeof raw.item.id === "string") payload.id = raw.item.id;
1021
- if (typeof raw.item.status === "string") payload.status = raw.item.status;
1011
+ // Claude Code, Codex, DeepSeek TUI, Kimi, and ACP all expose tool calls
1012
+ // with slightly different field names. Preserve the real invocation input
1013
+ // so the dashboard can show more than a bare "tool" label.
1014
+ const call = extractToolCall(raw);
1015
+ if (call) {
1016
+ payload.name = call.name;
1017
+ if (call.params !== undefined && !isEmptyRecord(call.params)) payload.params = call.params;
1018
+ if (call.id) payload.id = call.id;
1019
+ if (call.status) payload.status = call.status;
1022
1020
  }
1023
1021
  return { kind: "tool_call", seq, payload };
1024
1022
  }
1025
1023
 
1026
1024
  if (kind === "tool_result") {
1027
- // Claude Code: {type:"user", message:{content:[{type:"tool_result",tool_use_id,content}]}}
1028
- // Codex: item.completed for command_execution, file_change, mcp_tool_call, web_search
1029
- const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1030
- const tr = contents.find((c: any) => c?.type === "tool_result");
1031
- if (tr) {
1032
- let resultStr = "";
1033
- if (typeof tr.content === "string") {
1034
- resultStr = tr.content;
1035
- } else if (Array.isArray(tr.content)) {
1036
- resultStr = tr.content
1037
- .map((c: any) => (typeof c?.text === "string" ? c.text : JSON.stringify(c)))
1038
- .join("\n");
1039
- }
1040
- payload.result = resultStr;
1041
- if (typeof tr.tool_use_id === "string") payload.tool_use_id = tr.tool_use_id;
1042
- } else if (raw?.item && typeof raw.item === "object") {
1043
- payload.name = typeof raw.item.type === "string" ? raw.item.type : "tool";
1044
- if (typeof raw.item.id === "string") payload.tool_use_id = raw.item.id;
1045
- const result = codexToolResult(raw.item);
1046
- if (result) payload.result = result;
1025
+ const result = extractToolResult(raw);
1026
+ if (result) {
1027
+ if (result.name) payload.name = result.name;
1028
+ payload.result = result.result;
1029
+ if (result.id) payload.tool_use_id = result.id;
1047
1030
  }
1048
1031
  return { kind: "tool_result", seq, payload };
1049
1032
  }
@@ -1097,6 +1080,191 @@ function isTerminalRuntimeBlock(raw: any): boolean {
1097
1080
  );
1098
1081
  }
1099
1082
 
1083
+ function extractToolCall(raw: any): { name: string; params?: unknown; id?: string; status?: string } | null {
1084
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1085
+ const tu = contents.find((c: any) => c?.type === "tool_use");
1086
+ if (tu) {
1087
+ return {
1088
+ name: stringField(tu, "name") ?? "tool",
1089
+ params: parseMaybeJson(tu.input ?? tu.arguments),
1090
+ id: stringField(tu, "id"),
1091
+ };
1092
+ }
1093
+
1094
+ const deepseek = extractDeepseekToolCall(raw);
1095
+ if (deepseek) return deepseek;
1096
+
1097
+ const item = raw?.item;
1098
+ if (item && typeof item === "object") {
1099
+ const params = codexToolParams(item);
1100
+ return {
1101
+ name: stringField(item, "type") ?? stringField(item, "name") ?? "tool",
1102
+ params,
1103
+ id: stringField(item, "id"),
1104
+ status: stringField(item, "status"),
1105
+ };
1106
+ }
1107
+
1108
+ const toolCalls = Array.isArray(raw?.tool_calls) ? raw.tool_calls : [];
1109
+ const toolCall = toolCalls.find((t: any) => t && typeof t === "object");
1110
+ if (toolCall) {
1111
+ const fn = toolCall.function && typeof toolCall.function === "object" ? toolCall.function : undefined;
1112
+ return {
1113
+ name: stringField(fn, "name") ?? stringField(toolCall, "name") ?? "tool",
1114
+ params: parseMaybeJson(fn?.arguments ?? toolCall.arguments ?? toolCall.input ?? toolCall.rawInput),
1115
+ id: stringField(toolCall, "id"),
1116
+ };
1117
+ }
1118
+
1119
+ const update = raw?.params?.update ?? raw?.update;
1120
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
1121
+ if (acpTool && typeof acpTool === "object") {
1122
+ return {
1123
+ name: stringField(acpTool, "name") ?? stringField(update, "name") ?? "tool",
1124
+ params: parseMaybeJson(
1125
+ acpTool.rawInput ??
1126
+ acpTool.raw_input ??
1127
+ acpTool.input ??
1128
+ acpTool.arguments ??
1129
+ acpTool.args ??
1130
+ acpTool.params,
1131
+ ) ?? acpTool,
1132
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
1133
+ };
1134
+ }
1135
+
1136
+ return null;
1137
+ }
1138
+
1139
+ function extractToolResult(raw: any): { name?: string; result: string; id?: string } | null {
1140
+ const contents = Array.isArray(raw?.message?.content) ? raw.message.content : [];
1141
+ const tr = contents.find((c: any) => c?.type === "tool_result");
1142
+ if (tr) {
1143
+ return {
1144
+ result: stringifyToolResult(tr.content),
1145
+ id: stringField(tr, "tool_use_id"),
1146
+ };
1147
+ }
1148
+
1149
+ const deepseek = extractDeepseekToolResult(raw);
1150
+ if (deepseek) return deepseek;
1151
+
1152
+ const item = raw?.item;
1153
+ if (item && typeof item === "object") {
1154
+ const result = codexToolResult(item);
1155
+ return {
1156
+ name: stringField(item, "type") ?? stringField(item, "name"),
1157
+ result: result || stringifyToolResult(item),
1158
+ id: stringField(item, "id"),
1159
+ };
1160
+ }
1161
+
1162
+ if (raw?.role === "tool") {
1163
+ return {
1164
+ result: stringifyToolResult(raw.content),
1165
+ id: stringField(raw, "tool_call_id"),
1166
+ };
1167
+ }
1168
+
1169
+ const update = raw?.params?.update ?? raw?.update;
1170
+ const acpTool = update?.toolCall ?? update?.tool_call ?? update?.tool;
1171
+ if (acpTool && typeof acpTool === "object") {
1172
+ const result =
1173
+ acpTool.output ??
1174
+ acpTool.result ??
1175
+ acpTool.content ??
1176
+ acpTool.error ??
1177
+ update.content ??
1178
+ update;
1179
+ return {
1180
+ name: stringField(acpTool, "name") ?? stringField(update, "name"),
1181
+ result: stringifyToolResult(result),
1182
+ id: stringField(acpTool, "id") ?? stringField(update, "toolCallId"),
1183
+ };
1184
+ }
1185
+
1186
+ return null;
1187
+ }
1188
+
1189
+ function extractDeepseekToolCall(raw: any): { name: string; params?: unknown; id?: string; status?: string } | null {
1190
+ const payload = raw?.payload;
1191
+ if (!payload || typeof payload !== "object") return null;
1192
+
1193
+ if (raw?.event === "tool.started") {
1194
+ const tool = payload.tool && typeof payload.tool === "object" ? payload.tool : undefined;
1195
+ return {
1196
+ name: stringField(payload, "name") ?? stringField(tool, "name") ?? "tool",
1197
+ params: parseMaybeJson(payload.input ?? payload.arguments ?? payload.params ?? tool?.input ?? tool?.rawInput),
1198
+ id: stringField(payload, "id") ?? stringField(tool, "id"),
1199
+ status: stringField(payload, "status") ?? stringField(tool, "status"),
1200
+ };
1201
+ }
1202
+
1203
+ if (payload.event === "item.started") {
1204
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1205
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1206
+ const tool = inner.tool && typeof inner.tool === "object" ? inner.tool : item?.tool;
1207
+ return {
1208
+ name:
1209
+ stringField(tool, "name") ??
1210
+ stringField(inner, "name") ??
1211
+ stringField(item, "name") ??
1212
+ stringField(item, "type") ??
1213
+ "tool",
1214
+ params: parseMaybeJson(
1215
+ tool?.input ??
1216
+ tool?.rawInput ??
1217
+ tool?.arguments ??
1218
+ inner.input ??
1219
+ item?.input ??
1220
+ item?.arguments,
1221
+ ) ?? tool ?? item,
1222
+ id: stringField(tool, "id") ?? stringField(inner, "id") ?? stringField(item, "id"),
1223
+ status: stringField(tool, "status") ?? stringField(inner, "status") ?? stringField(item, "status"),
1224
+ };
1225
+ }
1226
+
1227
+ return null;
1228
+ }
1229
+
1230
+ function extractDeepseekToolResult(raw: any): { name?: string; result: string; id?: string } | null {
1231
+ const payload = raw?.payload;
1232
+ if (!payload || typeof payload !== "object") return null;
1233
+
1234
+ if (raw?.event === "tool.completed") {
1235
+ const result = payload.output ?? payload.result ?? payload.content ?? payload.error ?? payload;
1236
+ return {
1237
+ name: stringField(payload, "name"),
1238
+ result: stringifyToolResult(result),
1239
+ id: stringField(payload, "id"),
1240
+ };
1241
+ }
1242
+
1243
+ if (payload.event === "item.completed" || payload.event === "item.failed") {
1244
+ const inner = payload.payload && typeof payload.payload === "object" ? payload.payload : {};
1245
+ const item = inner.item && typeof inner.item === "object" ? inner.item : undefined;
1246
+ const result =
1247
+ item?.output ??
1248
+ item?.result ??
1249
+ item?.content ??
1250
+ item?.detail ??
1251
+ item?.summary ??
1252
+ item?.error ??
1253
+ inner.output ??
1254
+ inner.result ??
1255
+ inner.error ??
1256
+ item ??
1257
+ inner;
1258
+ return {
1259
+ name: stringField(item, "name") ?? stringField(item, "type") ?? stringField(inner, "name"),
1260
+ result: stringifyToolResult(result),
1261
+ id: stringField(item, "id") ?? stringField(inner, "id"),
1262
+ };
1263
+ }
1264
+
1265
+ return null;
1266
+ }
1267
+
1100
1268
  function formatBlockDetails(raw: unknown): string {
1101
1269
  if (!raw || typeof raw !== "object") return "";
1102
1270
  const r = raw as any;
@@ -1168,6 +1336,47 @@ function codexToolResult(item: Record<string, unknown>): string {
1168
1336
  return parts.join("\n");
1169
1337
  }
1170
1338
 
1339
+ function stringifyToolResult(value: unknown): string {
1340
+ if (value == null) return "";
1341
+ if (typeof value === "string") return value;
1342
+ if (Array.isArray(value)) {
1343
+ return value
1344
+ .map((c: any) => {
1345
+ if (typeof c === "string") return c;
1346
+ if (typeof c?.text === "string") return c.text;
1347
+ return stringifyToolResult(c);
1348
+ })
1349
+ .filter(Boolean)
1350
+ .join("\n");
1351
+ }
1352
+ try {
1353
+ return JSON.stringify(value, null, 2);
1354
+ } catch {
1355
+ return String(value);
1356
+ }
1357
+ }
1358
+
1359
+ function parseMaybeJson(value: unknown): unknown {
1360
+ if (typeof value !== "string") return value;
1361
+ const trimmed = value.trim();
1362
+ if (!trimmed) return value;
1363
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
1364
+ try {
1365
+ return JSON.parse(trimmed);
1366
+ } catch {
1367
+ return value;
1368
+ }
1369
+ }
1370
+
1371
+ function isEmptyRecord(value: unknown): boolean {
1372
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
1373
+ }
1374
+
1375
+ function stringField(obj: any, key: string): string | undefined {
1376
+ const value = obj?.[key];
1377
+ return typeof value === "string" && value.length > 0 ? value : undefined;
1378
+ }
1379
+
1171
1380
  function extractContentText(content: unknown): string {
1172
1381
  if (!content) return "";
1173
1382
  if (typeof content === "string") return content;
@@ -260,6 +260,9 @@ export class DeepseekTuiAdapter implements RuntimeAdapter {
260
260
  auto_approve: opts.trustLevel !== "public",
261
261
  archived: false,
262
262
  };
263
+ const selection = parseDeepseekRuntimeSelection(opts.extraArgs);
264
+ if (selection.model) body.model = selection.model;
265
+ if (selection.reasoningEffort) body.reasoning_effort = selection.reasoningEffort;
263
266
  if (opts.systemContext) body.system_prompt = opts.systemContext;
264
267
  const res = await this.requestJson<any>(`${baseUrl}/v1/threads`, {
265
268
  method: "POST",
@@ -306,18 +309,22 @@ export class DeepseekTuiAdapter implements RuntimeAdapter {
306
309
  });
307
310
  let turnId = "";
308
311
  try {
312
+ const selection = parseDeepseekRuntimeSelection(opts.extraArgs);
313
+ const body: Record<string, unknown> = {
314
+ prompt: opts.text,
315
+ mode: "agent",
316
+ allow_shell: opts.trustLevel !== "public",
317
+ trust_mode: opts.trustLevel !== "public",
318
+ auto_approve: opts.trustLevel !== "public",
319
+ };
320
+ if (selection.model) body.model = selection.model;
321
+ if (selection.reasoningEffort) body.reasoning_effort = selection.reasoningEffort;
309
322
  const started = await this.requestJson<any>(
310
323
  `${baseUrl}/v1/threads/${encodeURIComponent(threadId)}/turns`,
311
324
  {
312
325
  method: "POST",
313
326
  headers,
314
- body: JSON.stringify({
315
- prompt: opts.text,
316
- mode: "agent",
317
- allow_shell: opts.trustLevel !== "public",
318
- trust_mode: opts.trustLevel !== "public",
319
- auto_approve: opts.trustLevel !== "public",
320
- }),
327
+ body: JSON.stringify(body),
321
328
  signal,
322
329
  },
323
330
  );
@@ -535,6 +542,41 @@ function authHeaders(token: string): HeadersInit {
535
542
  return token ? { authorization: `Bearer ${token}` } : {};
536
543
  }
537
544
 
545
+ function parseDeepseekRuntimeSelection(
546
+ extraArgs: string[] | undefined,
547
+ ): { model?: string; reasoningEffort?: string } {
548
+ const out: { model?: string; reasoningEffort?: string } = {};
549
+ if (!extraArgs?.length) return out;
550
+ for (let i = 0; i < extraArgs.length; i += 1) {
551
+ const arg = extraArgs[i]!;
552
+ if (arg === "--model") {
553
+ const value = nextArgValue(extraArgs, i);
554
+ if (value !== undefined) {
555
+ out.model = value;
556
+ i += 1;
557
+ }
558
+ } else if (arg.startsWith("--model=")) {
559
+ out.model = arg.slice("--model=".length);
560
+ } else if (arg === "--reasoning-effort") {
561
+ const value = nextArgValue(extraArgs, i);
562
+ if (value !== undefined) {
563
+ out.reasoningEffort = value;
564
+ i += 1;
565
+ }
566
+ } else if (arg.startsWith("--reasoning-effort=")) {
567
+ out.reasoningEffort = arg.slice("--reasoning-effort=".length);
568
+ }
569
+ }
570
+ return out;
571
+ }
572
+
573
+ function nextArgValue(args: string[], index: number): string | undefined {
574
+ const next = args[index + 1];
575
+ if (typeof next !== "string") return undefined;
576
+ if (!next.startsWith("-")) return next;
577
+ return /^-\d/.test(next) ? next : undefined;
578
+ }
579
+
538
580
  function poolKey(opts: RuntimeRunOptions): string {
539
581
  return opts.accountId || "default";
540
582
  }
package/src/provision.ts CHANGED
@@ -72,6 +72,11 @@ import {
72
72
  import { log as daemonLog } from "./log.js";
73
73
  import { discoverAgentCredentials } from "./agent-discovery.js";
74
74
  import { resolveMemoryDir } from "./working-memory.js";
75
+ import { discoverRuntimeModelCatalog } from "./runtime-models.js";
76
+ import {
77
+ buildRuntimeSelectionExtraArgs,
78
+ mergeRuntimeExtraArgs,
79
+ } from "./runtime-route-options.js";
75
80
 
76
81
  /**
77
82
  * Information passed to {@link OnAgentInstalledHook} after a successful
@@ -1057,6 +1062,11 @@ function upsertManagedRouteForCredentials(
1057
1062
  runtime: credentials.runtime ?? cfg.defaultRoute.adapter,
1058
1063
  cwd: credentials.cwd ?? agentWorkspaceDir(credentials.agentId),
1059
1064
  };
1065
+ const extraArgs = mergeRuntimeExtraArgs(
1066
+ cfg.defaultRoute.extraArgs,
1067
+ buildRuntimeSelectionExtraArgs(synthRoute.runtime, credentials),
1068
+ );
1069
+ if (extraArgs) synthRoute.extraArgs = extraArgs;
1060
1070
  if (synthRoute.runtime === "openclaw-acp") {
1061
1071
  const profile = (cfg.openclawGateways ?? []).find(
1062
1072
  (g) => g.name === credentials.openclawGateway,
@@ -1169,6 +1179,10 @@ async function materializeCredentials(
1169
1179
  if (c.token) record.token = c.token;
1170
1180
  if (typeof c.tokenExpiresAt === "number") record.tokenExpiresAt = c.tokenExpiresAt;
1171
1181
  if (runtime) record.runtime = runtime;
1182
+ const runtimeSelection = pickRuntimeSelection(params);
1183
+ if (runtimeSelection.runtimeModel) record.runtimeModel = runtimeSelection.runtimeModel;
1184
+ if (runtimeSelection.reasoningEffort) record.reasoningEffort = runtimeSelection.reasoningEffort;
1185
+ if (typeof runtimeSelection.thinking === "boolean") record.thinking = runtimeSelection.thinking;
1172
1186
  record.cwd = cwd;
1173
1187
  const openclawSel = pickOpenclawSelection(params);
1174
1188
  if (openclawSel.gateway) record.openclawGateway = openclawSel.gateway;
@@ -1203,6 +1217,10 @@ async function materializeCredentials(
1203
1217
  tokenExpiresAt: reg.expiresAt,
1204
1218
  };
1205
1219
  if (runtime) record.runtime = runtime;
1220
+ const runtimeSelection = pickRuntimeSelection(params);
1221
+ if (runtimeSelection.runtimeModel) record.runtimeModel = runtimeSelection.runtimeModel;
1222
+ if (runtimeSelection.reasoningEffort) record.reasoningEffort = runtimeSelection.reasoningEffort;
1223
+ if (typeof runtimeSelection.thinking === "boolean") record.thinking = runtimeSelection.thinking;
1206
1224
  record.cwd = cwd;
1207
1225
  const openclawSel = pickOpenclawSelection(params);
1208
1226
  if (openclawSel.gateway) record.openclawGateway = openclawSel.gateway;
@@ -1784,6 +1802,10 @@ export function collectRuntimeSnapshot(opts: { force?: boolean } = {}): ListRunt
1784
1802
  // style used above.
1785
1803
  if (entry.result.version) record.version = entry.result.version;
1786
1804
  if (entry.result.path) record.path = entry.result.path;
1805
+ const catalog = discoverRuntimeModelCatalog(entry);
1806
+ const models = catalog.models;
1807
+ if (models?.length) record.models = models.slice(0, RUNTIME_MODELS_CAP);
1808
+ if (catalog.parameters?.length) record.parameters = catalog.parameters.slice(0, RUNTIME_PARAMETERS_CAP);
1787
1809
  // Gateway's probe surface doesn't expose an `error` string today — it
1788
1810
  // already swallows throws into `{available: false}`. We leave the wire
1789
1811
  // field blank in that case and let callers treat `!available` as reason
@@ -1841,6 +1863,8 @@ export function attachRuntimeHealth(
1841
1863
 
1842
1864
  /** Maximum number of `endpoints[]` entries persisted per runtime (RFC §3.8.2). */
1843
1865
  export const RUNTIME_ENDPOINTS_CAP = 32;
1866
+ export const RUNTIME_MODELS_CAP = 128;
1867
+ export const RUNTIME_PARAMETERS_CAP = 64;
1844
1868
 
1845
1869
  /** Injection seam for L2 + L3 endpoint probes — kept testable + side-effect-free. */
1846
1870
  export type WsEndpointProbeFn = (args: {
@@ -2511,20 +2535,34 @@ export async function reloadConfig(ctx: { gateway: Gateway }): Promise<ReloadRes
2511
2535
  */
2512
2536
  function readAgentRuntimesFromCredentials(
2513
2537
  agentIds: string[],
2514
- ): Record<string, { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }> {
2515
- const out: Record<string, { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }> = {};
2538
+ ): Record<string, { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }> {
2539
+ const out: Record<string, { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string }> = {};
2516
2540
  for (const id of agentIds) {
2517
2541
  const file = defaultCredentialsFile(id);
2518
2542
  try {
2519
2543
  if (!existsSync(file)) continue;
2520
2544
  const creds = loadStoredCredentials(file);
2521
- const entry: { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string } = {};
2545
+ const entry: { runtime?: string; runtimeModel?: string; reasoningEffort?: string; thinking?: boolean; cwd?: string; openclawGateway?: string; openclawAgent?: string; hermesProfile?: string } = {};
2522
2546
  if (creds.runtime) entry.runtime = creds.runtime;
2547
+ if (creds.runtimeModel) entry.runtimeModel = creds.runtimeModel;
2548
+ if (creds.reasoningEffort) entry.reasoningEffort = creds.reasoningEffort;
2549
+ if (typeof creds.thinking === "boolean") entry.thinking = creds.thinking;
2523
2550
  if (creds.cwd) entry.cwd = creds.cwd;
2524
2551
  if (creds.openclawGateway) entry.openclawGateway = creds.openclawGateway;
2525
2552
  if (creds.openclawAgent) entry.openclawAgent = creds.openclawAgent;
2526
2553
  if (creds.hermesProfile) entry.hermesProfile = creds.hermesProfile;
2527
- if (entry.runtime || entry.cwd || entry.openclawGateway || entry.openclawAgent || entry.hermesProfile) out[id] = entry;
2554
+ if (
2555
+ entry.runtime ||
2556
+ entry.runtimeModel ||
2557
+ entry.reasoningEffort ||
2558
+ typeof entry.thinking === "boolean" ||
2559
+ entry.cwd ||
2560
+ entry.openclawGateway ||
2561
+ entry.openclawAgent ||
2562
+ entry.hermesProfile
2563
+ ) {
2564
+ out[id] = entry;
2565
+ }
2528
2566
  } catch {
2529
2567
  // best-effort — skip agents with unreadable credentials
2530
2568
  }
@@ -2769,6 +2807,33 @@ function pickRuntime(params: ProvisionAgentParams): string | undefined {
2769
2807
  return undefined;
2770
2808
  }
2771
2809
 
2810
+ function pickRuntimeSelection(
2811
+ params: ProvisionAgentParams,
2812
+ ): { runtimeModel?: string; reasoningEffort?: string; thinking?: boolean } {
2813
+ const out: { runtimeModel?: string; reasoningEffort?: string; thinking?: boolean } = {};
2814
+ const runtimeModel = pickString(params.runtimeModel, params.credentials?.runtimeModel);
2815
+ const reasoningEffort = pickString(
2816
+ params.reasoningEffort,
2817
+ params.credentials?.reasoningEffort,
2818
+ );
2819
+ if (runtimeModel) out.runtimeModel = runtimeModel;
2820
+ if (reasoningEffort) out.reasoningEffort = reasoningEffort;
2821
+ if (typeof params.thinking === "boolean") {
2822
+ out.thinking = params.thinking;
2823
+ } else if (typeof params.credentials?.thinking === "boolean") {
2824
+ out.thinking = params.credentials.thinking;
2825
+ }
2826
+ return out;
2827
+ }
2828
+
2829
+ function pickString(...values: Array<string | undefined>): string | undefined {
2830
+ for (const value of values) {
2831
+ const trimmed = value?.trim();
2832
+ if (trimmed) return trimmed;
2833
+ }
2834
+ return undefined;
2835
+ }
2836
+
2772
2837
  function assertKnownRuntime(runtime: string): void {
2773
2838
  const mod = getAdapterModule(runtime);
2774
2839
  if (!mod) {