@cydm/happy-elves 0.1.0-beta.50 → 0.1.0-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/apps/cli/dist/commands/lib/args.js +3 -0
  2. package/apps/cli/dist/commands/lib/orchestrator.d.ts +2 -0
  3. package/apps/cli/dist/commands/lib/orchestrator.js +2 -0
  4. package/apps/cli/dist/commands/lib/scope.d.ts +2 -0
  5. package/apps/cli/dist/commands/lib/scope.js +18 -0
  6. package/apps/cli/dist/commands/lib/usage.js +5 -5
  7. package/apps/cli/dist/commands/orchestrator.js +30 -7
  8. package/apps/cli/dist/commands/session.js +4 -2
  9. package/apps/cli/dist/commands/skill.js +4 -2
  10. package/apps/cli/dist/commands/turn.js +4 -2
  11. package/apps/daemon/dist/relay/register.js +1 -0
  12. package/apps/daemon/dist/runtime/external-provider.js +8 -2
  13. package/apps/daemon/dist/session/lifecycle.js +5 -0
  14. package/apps/daemon/dist/session/prompt.js +13 -3
  15. package/apps/daemon/dist/types.d.ts +4 -1
  16. package/apps/daemon/package.json +1 -1
  17. package/apps/relay/dist/connections.d.ts +2 -0
  18. package/apps/relay/dist/controller-handlers.js +32 -1
  19. package/apps/relay/dist/projections.js +33 -0
  20. package/apps/relay/dist/relay-context.js +9 -0
  21. package/npm-shrinkwrap.json +2 -2
  22. package/package.json +1 -1
  23. package/packages/agent-sdk/dist/index.d.ts +7 -1
  24. package/packages/client/dist/client.js +4 -0
  25. package/packages/client/dist/live-types.d.ts +2 -0
  26. package/packages/client/dist/live.js +2 -0
  27. package/packages/client/dist/types.d.ts +8 -1
  28. package/packages/pie-provider/dist/index.js +59 -4
  29. package/packages/provider-protocol/dist/index.d.ts +67 -0
  30. package/packages/provider-protocol/dist/index.js +16 -0
  31. package/packages/runtime/dist/index.d.ts +8 -2
  32. package/packages/runtime-cli/dist/claude-turn.d.ts +2 -1
  33. package/packages/runtime-cli/dist/claude-turn.js +31 -3
  34. package/packages/runtime-cli/dist/codex-app-server.d.ts +11 -3
  35. package/packages/runtime-cli/dist/codex-app-server.js +143 -21
  36. package/packages/runtime-cli/dist/codex-protocol.d.ts +5 -3
  37. package/packages/runtime-cli/dist/codex-protocol.js +23 -5
  38. package/packages/runtime-cli/dist/index.js +79 -15
  39. package/packages/runtime-cli/dist/session-store.d.ts +2 -0
  40. package/packages/runtime-cli/dist/session-store.js +6 -4
  41. package/packages/shared/dist/protocol-schemas.d.ts +28 -0
  42. package/packages/shared/dist/protocol-schemas.js +12 -0
  43. package/packages/shared/dist/protocol-types.d.ts +12 -0
  44. package/packages/shared/dist/protocol.d.ts +5 -1
  45. package/packages/shared/dist/protocol.js +4 -0
@@ -16,6 +16,7 @@ const valueFlags = new Set([
16
16
  "machine-id",
17
17
  "name",
18
18
  "mode",
19
+ "model",
19
20
  "output",
20
21
  "path",
21
22
  "port",
@@ -49,6 +50,8 @@ const valueFlags = new Set([
49
50
  "text-file",
50
51
  "timeout",
51
52
  "timeout-ms",
53
+ "thought-level",
54
+ "thinking",
52
55
  "wait-timeout-ms",
53
56
  "runtime-timeout",
54
57
  "to",
@@ -6,6 +6,8 @@ export type OrchestratorStatus = "idle" | "inProgress" | "completed" | "needsInp
6
6
  export type OrchestratorRequestRecord = {
7
7
  turnId: string;
8
8
  promptSha256: string;
9
+ model?: string | null;
10
+ thoughtLevel?: string | null;
9
11
  createdAt: number;
10
12
  };
11
13
  export type OrchestratorMetadata = {
@@ -77,6 +77,8 @@ export function readOrchestratorMetadata(metadata) {
77
77
  requests[requestId] = {
78
78
  turnId: candidate.turnId,
79
79
  promptSha256: candidate.promptSha256,
80
+ ...(candidate.model === null || typeof candidate.model === "string" ? { model: candidate.model } : {}),
81
+ ...(candidate.thoughtLevel === null || typeof candidate.thoughtLevel === "string" ? { thoughtLevel: candidate.thoughtLevel } : {}),
80
82
  createdAt: candidate.createdAt,
81
83
  };
82
84
  }
@@ -9,6 +9,8 @@ export declare function requireScopedLoop(scope: ScopedTokenScope | undefined, l
9
9
  export declare function loopVisibleToScope(loop: LoopRecord, scope: ScopedTokenScope | undefined, tokenId?: string, machineId?: string | undefined): boolean;
10
10
  export declare function parseEventCursor(value: string | boolean | undefined): `cursor_${number}` | number | undefined;
11
11
  export declare function parsePermissionMode(value: string | boolean | undefined): PermissionMode | undefined;
12
+ export declare function parseRuntimeSelection(value: string | boolean | undefined, label: string): string | null | undefined;
13
+ export declare function parseThoughtLevel(flags: Record<string, string | boolean>): string | null | undefined;
12
14
  export declare function parseControllerJoinUrl(value: string): {
13
15
  relayUrl: string;
14
16
  code: string;
@@ -82,6 +82,24 @@ export function parsePermissionMode(value) {
82
82
  return "approve-reads";
83
83
  throw new CliError(`Invalid permission mode: ${value}`, "INVALID_ARGUMENT");
84
84
  }
85
+ export function parseRuntimeSelection(value, label) {
86
+ if (value === undefined || value === false)
87
+ return undefined;
88
+ if (value === true)
89
+ throw new CliError(`Missing --${label} value`, "MISSING_ARGUMENT");
90
+ const normalized = value.trim();
91
+ if (!normalized)
92
+ throw new CliError(`Invalid --${label}: empty value`, "INVALID_ARGUMENT");
93
+ return normalized.toLowerCase() === "default" ? null : normalized;
94
+ }
95
+ export function parseThoughtLevel(flags) {
96
+ const canonical = flags["thought-level"];
97
+ const alias = flags.thinking;
98
+ if (canonical !== undefined && canonical !== false && alias !== undefined && alias !== false) {
99
+ throw new CliError("Use only one of --thought-level or --thinking", "INVALID_ARGUMENT");
100
+ }
101
+ return parseRuntimeSelection(canonical !== undefined && canonical !== false ? canonical : alias, canonical !== undefined && canonical !== false ? "thought-level" : "thinking");
102
+ }
85
103
  export function parseControllerJoinUrl(value) {
86
104
  let url;
87
105
  try {
@@ -47,7 +47,7 @@ Check local controller config, relay reachability, and daemon state.
47
47
  orchestrator: `Usage:
48
48
  happy-elves orchestrator list-threads [--query <q>] [--tag <tag>] [--machine <id>] [--limit n] [--all] --json
49
49
  happy-elves orchestrator read-thread <threadId> [--messages latest|all] [--limit n] [--include-output] --json
50
- happy-elves orchestrator send-message <threadId> (--text <prompt> | --text-file <path>) --request-id <id> [--permission-mode <mode>] [--runtime-timeout 1000s] --json
50
+ happy-elves orchestrator send-message <threadId> (--text <prompt> | --text-file <path>) --request-id <id> [--permission-mode <mode>] [--model <key>] [--thought-level <key>|--thinking <key>] [--runtime-timeout 1000s] --json
51
51
  happy-elves orchestrator wait-thread <threadId> --turn <turnId> [--timeout 1000s] --json
52
52
  happy-elves orchestrator set-thread-tags <threadId> --tags worker,reviewer --json
53
53
 
@@ -57,7 +57,7 @@ Codex-like orchestration:
57
57
  for that exact turn id instead of following the latest turn.
58
58
  `,
59
59
  session: `Usage:
60
- happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] --json
60
+ happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] [--model <key>] [--thought-level <key>|--thinking <key>] --json
61
61
  happy-elves session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] [--limit n] [--all] [--verbose] [--json]
62
62
  happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
63
63
  happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
@@ -85,7 +85,7 @@ Session management:
85
85
  happy-elves turn list <sessionId> [--limit 10] [--verbose] [--json]
86
86
  happy-elves turn status <sessionId> <turnId> [--verbose] [--json]
87
87
  happy-elves turn result <sessionId> [<turnId>] [--verbose] [--json]
88
- happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--runtime-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--json]
88
+ happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--runtime-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--model <key>] [--thought-level <key>|--thinking <key>] [--json]
89
89
  happy-elves turn wait <sessionId> <turnId> [--timeout 1000s] --json
90
90
  happy-elves turn cancel <sessionId> <turnId> [--reason <reason>] --json
91
91
  happy-elves turn rewind <sessionId> <turn-or-checkpoint-id> --json
@@ -260,8 +260,8 @@ Output modes:
260
260
  thread's history is unavailable, the item reports historyStatus/historyError
261
261
  instead of pretending to be healthy inProgress.
262
262
  send-message requires --request-id and is detached by default. Reusing the
263
- same request id with the same prompt returns the same turn; reusing it with a
264
- different prompt returns REQUEST_ID_CONFLICT.
263
+ same request id with the same prompt/model/thinking returns the same turn;
264
+ reusing it with different prompt/model/thinking returns REQUEST_ID_CONFLICT.
265
265
 
266
266
  Debug commands:
267
267
  machine/session/turn commands remain available for lower-level inspection,
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, conciseEvents, deterministicRelayRequestId, deterministicTurnId, deriveOrchestration, normalizeOrchestratorStatus, normalizeOrchestratorTag, normalizeOrchestratorTags, ok, parseDurationMs, parsePermissionMode, promptSha256, readConfig, readOrchestratorMetadata, readPromptTextFile, rebuildTurnOutput, requirePositional, requireString, summarizeTurns, withOrchestratorRequest, withOrchestratorTags, } from "./lib/index.js";
1
+ import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, conciseEvents, deterministicRelayRequestId, deterministicTurnId, deriveOrchestration, normalizeOrchestratorStatus, normalizeOrchestratorTag, normalizeOrchestratorTags, ok, parseDurationMs, parsePermissionMode, parseRuntimeSelection, parseThoughtLevel, promptSha256, readConfig, readOrchestratorMetadata, readPromptTextFile, rebuildTurnOutput, requirePositional, requireString, summarizeTurns, withOrchestratorRequest, withOrchestratorTags, } from "./lib/index.js";
2
2
  const defaultTurnHistoryLimit = 1000;
3
3
  const defaultMessageLimit = 50;
4
4
  class OrchestratorError extends CliError {
@@ -101,15 +101,28 @@ async function readThreadContext(client, threadId) {
101
101
  }
102
102
  return { machines: snapshot.machines, session };
103
103
  }
104
- function requestRecordForExistingTurn(turn, hash, createdAt) {
104
+ function requestRecordForExistingTurn(turn, hash, selection, createdAt) {
105
105
  if (!turn)
106
106
  return undefined;
107
107
  return {
108
108
  turnId: turn.turnId,
109
109
  promptSha256: hash,
110
+ ...selectionRecordFields(selection),
110
111
  createdAt: turn.startedAt ?? turn.updatedAt ?? createdAt,
111
112
  };
112
113
  }
114
+ function selectionRecordFields(selection) {
115
+ return {
116
+ ...(selection.model !== undefined ? { model: selection.model } : {}),
117
+ ...(selection.thoughtLevel !== undefined ? { thoughtLevel: selection.thoughtLevel } : {}),
118
+ };
119
+ }
120
+ function sameRequestRecord(existing, expected) {
121
+ return existing.turnId === expected.turnId &&
122
+ existing.promptSha256 === expected.promptSha256 &&
123
+ existing.model === expected.model &&
124
+ existing.thoughtLevel === expected.thoughtLevel;
125
+ }
113
126
  function errorCode(error) {
114
127
  const candidate = error;
115
128
  return typeof candidate?.code === "string" ? candidate.code : undefined;
@@ -148,7 +161,7 @@ async function persistRequestRecordBestEffort(client, threadId, requestId, recor
148
161
  const metadata = await client.decodeSessionMetadata(session);
149
162
  const existing = readOrchestratorMetadata(metadata).requests?.[requestId];
150
163
  if (existing) {
151
- if (existing.turnId === record.turnId && existing.promptSha256 === record.promptSha256)
164
+ if (sameRequestRecord(existing, record))
152
165
  return;
153
166
  return;
154
167
  }
@@ -172,7 +185,7 @@ function terminalStatusForTurn(turn) {
172
185
  return "inProgress";
173
186
  }
174
187
  function requestConflict(threadId, requestId, turnId) {
175
- return new OrchestratorError(`Request id already exists with different prompt content: ${requestId}`, "REQUEST_ID_CONFLICT", { retryable: false, sessionId: threadId, turnId });
188
+ return new OrchestratorError(`Request id already exists with different prompt, model, or thoughtLevel: ${requestId}`, "REQUEST_ID_CONFLICT", { retryable: false, sessionId: threadId, turnId });
176
189
  }
177
190
  function threadBusy(threadId, active, latestCompleted) {
178
191
  return new OrchestratorError(`Thread already has a running turn: ${active.turnId}`, "THREAD_BUSY", {
@@ -284,11 +297,19 @@ export async function handleOrchestrator({ domain, action, positional, flags })
284
297
  const turnId = deterministicTurnId(threadId, requestId);
285
298
  const relayRequestId = deterministicRelayRequestId(threadId, requestId);
286
299
  const hash = promptSha256(text);
300
+ const model = parseRuntimeSelection(flags.model, "model");
301
+ const thoughtLevel = parseThoughtLevel(flags);
302
+ const selection = selectionRecordFields({ model, thoughtLevel });
303
+ const record = { turnId, promptSha256: hash, ...selection, createdAt: Date.now() };
287
304
  const self = await client.tokenSelf();
288
305
  if (!self.full) {
289
306
  throw new OrchestratorError("orchestrator send-message requires a full controller token.", "SCOPED_TOKEN_FORBIDDEN", { retryable: false, sessionId: threadId, turnId });
290
307
  }
291
- await readThreadContext(client, threadId);
308
+ const { session } = await readThreadContext(client, threadId);
309
+ const metadata = await client.decodeSessionMetadata(session);
310
+ const existingRequest = readOrchestratorMetadata(metadata).requests?.[requestId];
311
+ if (existingRequest && !sameRequestRecord(existingRequest, record))
312
+ throw requestConflict(threadId, requestId, turnId);
292
313
  const events = await client.history(threadId, defaultTurnHistoryLimit);
293
314
  const turns = summarizeTurns(events);
294
315
  const existingTurn = await readExactTurn(client, threadId, turnId);
@@ -299,9 +320,10 @@ export async function handleOrchestrator({ domain, action, positional, flags })
299
320
  if (active && active.turnId !== turnId)
300
321
  throw threadBusy(threadId, active, latestCompleted);
301
322
  if (existingTurn && existingTurn.status !== "running") {
302
- await persistRequestRecordBestEffort(client, threadId, requestId, requestRecordForExistingTurn(existingTurn, hash, Date.now()) ?? {
323
+ await persistRequestRecordBestEffort(client, threadId, requestId, requestRecordForExistingTurn(existingTurn, hash, selection, Date.now()) ?? {
303
324
  turnId,
304
325
  promptSha256: hash,
326
+ ...selection,
305
327
  createdAt: Date.now(),
306
328
  });
307
329
  ok("orchestrator.sendMessage", {
@@ -326,7 +348,6 @@ export async function handleOrchestrator({ domain, action, positional, flags })
326
348
  }, { requestId, sessionId: threadId, turnId });
327
349
  return true;
328
350
  }
329
- const record = { turnId, promptSha256: hash, createdAt: Date.now() };
330
351
  const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
331
352
  const runtimeTimeoutMs = parseDurationMs(flags["runtime-timeout"], DEFAULT_COMMAND_TIMEOUT_MS);
332
353
  try {
@@ -336,6 +357,8 @@ export async function handleOrchestrator({ domain, action, positional, flags })
336
357
  requestId: relayRequestId,
337
358
  turnId,
338
359
  promptHash: hash,
360
+ model,
361
+ thoughtLevel,
339
362
  ackTimeoutMs: 15_000,
340
363
  runtimeTimeoutMs,
341
364
  });
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, compareSessionEventLogicalOrder, compactSessionListItem, compactText, conciseSessionSummary, deriveOrchestration, listWorkspaceHistoricalSessions, ok, parsePermissionMode, readConfig, readStoredTurnOutput, requirePositional, requireString, showSession, structuredSessionProjection, summarizeTurns, wantsJson, wantsVerbose } from "./lib/index.js";
1
+ import { CliError, ControllerClient, compareSessionEventLogicalOrder, compactSessionListItem, compactText, conciseSessionSummary, deriveOrchestration, listWorkspaceHistoricalSessions, ok, parsePermissionMode, parseRuntimeSelection, parseThoughtLevel, readConfig, readStoredTurnOutput, requirePositional, requireString, showSession, structuredSessionProjection, summarizeTurns, wantsJson, wantsVerbose } from "./lib/index.js";
2
2
  function formatTurnTimestamp(value) {
3
3
  return value === undefined ? "-" : new Date(value).toISOString();
4
4
  }
@@ -355,8 +355,10 @@ export async function handleSession({ domain, action, positional, flags }) {
355
355
  const requestedRuntimeName = typeof flags.name === "string" ? flags.name : undefined;
356
356
  const name = "main";
357
357
  const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
358
+ const model = parseRuntimeSelection(flags.model, "model");
359
+ const thoughtLevel = parseThoughtLevel(flags);
358
360
  const client = await makeClient();
359
- const result = await client.createSession({ machineId, agent, cwd, name, runtimeName: requestedRuntimeName, permissionMode });
361
+ const result = await client.createSession({ machineId, agent, cwd, name, runtimeName: requestedRuntimeName, permissionMode, model, thoughtLevel });
360
362
  const session = result.sessionId ? await client.getSession(result.sessionId) : undefined;
361
363
  const projectedSession = session
362
364
  ? wantsVerbose(flags)
@@ -42,8 +42,10 @@ happy-elves orchestrator wait-thread <threadId> --turn <turnId> --json
42
42
  Important rules:
43
43
  - \`threadId\` is the same value as Happy Elves \`sessionId\`.
44
44
  - Use stable request ids such as \`orch_round_3_worker_fix\`.
45
- - Reusing the same request id with the same prompt returns the same turn.
46
- - Reusing the same request id with different prompt text returns \`REQUEST_ID_CONFLICT\`.
45
+ - Reusing the same request id with the same prompt/model/thinking returns the same turn.
46
+ - Reusing the same request id with different prompt text, model, or thinking returns \`REQUEST_ID_CONFLICT\`.
47
+ - To override runtime selection, pass \`--model <key>\` and \`--thought-level <key>\`; \`--thinking <key>\` is an alias. Use \`default\` to return to provider defaults.
48
+ - Runtime failures such as \`CODEX_EMPTY_COMPLETION\` are terminal failed turns; inspect \`wait-thread --turn\` or \`turn result\` for the full error text.
47
49
  - If \`send-message\` returns \`THREAD_BUSY\`, wait/read the active turn instead of sending another prompt.
48
50
  - For waiting, use the exact \`turnId\` returned by \`send-message\`; do not follow "latest" in multi-actor workflows.
49
51
  - After installing or updating this skill, open a fresh Codex thread or reload skills before relying on the new instructions.
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, displayPath, ok, parseDurationMs, parsePermissionMode, printConciseEvents, readConfig, readPromptTextFile, rebuildTurnOutput, requirePositional, requireProjectedSession, requireString, structuredOutputForRunningTurn, structuredOutputForStoredTurn, structuredOutputForTurn, summarizeTurns, wantsJson, wantsVerbose, writeTurnOutput } from "./lib/index.js";
1
+ import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, displayPath, ok, parseDurationMs, parsePermissionMode, parseRuntimeSelection, parseThoughtLevel, printConciseEvents, readConfig, readPromptTextFile, rebuildTurnOutput, requirePositional, requireProjectedSession, requireString, structuredOutputForRunningTurn, structuredOutputForStoredTurn, structuredOutputForTurn, summarizeTurns, wantsJson, wantsVerbose, writeTurnOutput } from "./lib/index.js";
2
2
  function formatTurnTimestamp(value) {
3
3
  return value === undefined ? "-" : new Date(value).toISOString();
4
4
  }
@@ -120,11 +120,13 @@ export async function handleTurn({ domain, action, positional, flags }) {
120
120
  if (!text.trim())
121
121
  throw new CliError("Missing --text or --text-file", "MISSING_ARGUMENT");
122
122
  const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
123
+ const model = parseRuntimeSelection(flags.model, "model");
124
+ const thoughtLevel = parseThoughtLevel(flags);
123
125
  const wait = flags.detach !== true && flags["no-wait"] !== true;
124
126
  const watchTimeoutMs = parseDurationMs(flags.timeout, DEFAULT_COMMAND_TIMEOUT_MS);
125
127
  const ackTimeoutMs = 15_000;
126
128
  const runtimeTimeoutMs = parseDurationMs(flags["runtime-timeout"], DEFAULT_COMMAND_TIMEOUT_MS);
127
- const result = await client.run(sessionId, text, { wait, permissionMode, waitTimeoutMs: watchTimeoutMs, ackTimeoutMs, runtimeTimeoutMs });
129
+ const result = await client.run(sessionId, text, { wait, permissionMode, model, thoughtLevel, waitTimeoutMs: watchTimeoutMs, ackTimeoutMs, runtimeTimeoutMs });
128
130
  const events = await client.decodeEvents(result.events);
129
131
  const output = wait && result.turnId ? await writeTurnOutput(sessionId, result.turnId, events) : undefined;
130
132
  if (wantsJson(flags)) {
@@ -24,6 +24,7 @@ function agentProfileForRegister(agent) {
24
24
  ...(agent.command ? { command: agent.command } : {}),
25
25
  ...(agent.historyModel ? { historyModel: agent.historyModel } : {}),
26
26
  ...(agent.sessionPrimitives ? { sessionPrimitives: runtimeAgentPrimitives(agent.sessionPrimitives) } : {}),
27
+ ...(agent.runtimeOptions ? { runtimeOptions: agent.runtimeOptions } : {}),
27
28
  };
28
29
  }
29
30
  function runtimeAgentPrimitives(primitives) {
@@ -10,7 +10,7 @@ const maxJsonLineChars = 1024 * 1024;
10
10
  const stderrRingChars = 16 * 1024;
11
11
  const historicalEventPageLimit = 200;
12
12
  const historicalEventPageSafetyLimit = 1_000;
13
- const pieMinimumVersion = "1.0.20";
13
+ const pieMinimumVersion = "1.0.21";
14
14
  const pieAutoProbeTimeoutMs = 5_000;
15
15
  export function externalProviderConfigsFromEnv() {
16
16
  const raw = process.env.HAPPY_ELVES_AGENT_PROVIDERS_JSON?.trim();
@@ -74,11 +74,12 @@ function pieCommandSupportsProvider(command) {
74
74
  const listResult = runPieProbe(command, ["sessions", "list", "--json", "--limit", "1"]);
75
75
  if (listResult.status !== 0)
76
76
  return false;
77
- const modelsResult = runPieProbe(command, ["models", "validate"]);
77
+ const modelsResult = runPieProbe(command, ["models", "validate", "--json"]);
78
78
  if (modelsResult.status !== 0)
79
79
  return false;
80
80
  try {
81
81
  JSON.parse(listResult.stdout);
82
+ JSON.parse(modelsResult.stdout);
82
83
  return true;
83
84
  }
84
85
  catch {
@@ -315,6 +316,7 @@ export function createExternalAgentRuntimeProvider(options) {
315
316
  rename: agent.capabilities.sessionPrimitives.rename ?? "unsupported",
316
317
  }
317
318
  : { resume: "unsupported", rewind: "unsupported", fork: "unsupported", rename: "unsupported" },
319
+ runtimeOptions: agent.capabilities?.runtimeOptions,
318
320
  }));
319
321
  const agentsById = new Map();
320
322
  for (const agent of externalAgents)
@@ -338,6 +340,8 @@ export function createExternalAgentRuntimeProvider(options) {
338
340
  ...(input.resumeHandle ? { resumeHandle: sdkHandle(input.resumeHandle) } : {}),
339
341
  cwd: input.cwd,
340
342
  permissionMode: input.permissionMode,
343
+ model: input.model,
344
+ thoughtLevel: input.thoughtLevel,
341
345
  restartableEmptySession: input.restartableEmptySession,
342
346
  }), "ensureSession result handle");
343
347
  return toRuntimeHandle(input.sessionKey, handle, input.cwd);
@@ -429,6 +433,8 @@ export function createExternalAgentRuntimeProvider(options) {
429
433
  attachments: input.attachments,
430
434
  mode: input.mode,
431
435
  permissionMode: input.permissionMode,
436
+ model: input.model,
437
+ thoughtLevel: input.thoughtLevel,
432
438
  requestId: input.requestId,
433
439
  timeoutMs: input.timeoutMs,
434
440
  });
@@ -22,6 +22,8 @@ export async function handleCreateSession(ws, config, command) {
22
22
  mode: "persistent",
23
23
  cwd,
24
24
  permissionMode: metadata?.permissionMode,
25
+ model: metadata?.modelMode,
26
+ thoughtLevel: metadata?.thoughtLevel,
25
27
  });
26
28
  let providerName;
27
29
  const requestedRuntimeName = metadata?.requestedRuntimeName;
@@ -68,6 +70,9 @@ export async function handleCreateSession(ws, config, command) {
68
70
  ...(providerName ? { name: providerName } : {}),
69
71
  encryptedMetadata: await encryptedSessionMetadata(config, command.sessionId, state, {
70
72
  emptyRuntimeSession: true,
73
+ modelMode: metadata?.modelMode,
74
+ thoughtLevel: metadata?.thoughtLevel,
75
+ runtimeOptions: handle.runtimeOptions,
71
76
  ...(providerName ? { providerName, renamedAt: new Date().toISOString() } : {}),
72
77
  readyAt: new Date().toISOString(),
73
78
  }),
@@ -6,7 +6,7 @@ import { activePromptRequests, completedPromptRequests, sessions, sessionTurns }
6
6
  import { attachClaimedTurn, claimSessionTurn, releaseSessionTurn, sendCommandResponse, sendReliable } from "../relay/send.js";
7
7
  import { emitEvent, runtimeEventToPayload } from "./events.js";
8
8
  import { replaceAuthoritativeTranscript, resumeSession, usesAuthoritativeTranscript } from "./lifecycle.js";
9
- import { encryptedSessionMetadata, sessionFallbackFromCommand } from "./metadata.js";
9
+ import { commandSessionMetadata, encryptedSessionMetadata, sessionFallbackFromCommand } from "./metadata.js";
10
10
  export async function handlePrompt(ws, config, command) {
11
11
  if (command.type !== "machine:prompt")
12
12
  return;
@@ -47,6 +47,9 @@ export async function handlePrompt(ws, config, command) {
47
47
  };
48
48
  try {
49
49
  const previousBackendSessionId = session.handle.backendSessionId;
50
+ const previousMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
51
+ const effectiveModel = command.model !== undefined ? command.model : previousMetadata?.modelMode;
52
+ const effectiveThoughtLevel = command.thoughtLevel !== undefined ? command.thoughtLevel : previousMetadata?.thoughtLevel;
50
53
  let result;
51
54
  try {
52
55
  const prompt = await decryptJson(config.accountSecret, command.encryptedPrompt);
@@ -60,6 +63,8 @@ export async function handlePrompt(ws, config, command) {
60
63
  text,
61
64
  mode: "prompt",
62
65
  permissionMode: command.permissionMode,
66
+ model: effectiveModel,
67
+ thoughtLevel: effectiveThoughtLevel,
63
68
  requestId: command.requestId,
64
69
  timeoutMs: command.timeoutMs,
65
70
  });
@@ -78,6 +83,8 @@ export async function handlePrompt(ws, config, command) {
78
83
  turnId: command.turnId,
79
84
  promptChars: text.length,
80
85
  permissionMode: command.permissionMode ?? "approve-reads",
86
+ ...(effectiveModel !== undefined ? { model: effectiveModel } : {}),
87
+ ...(effectiveThoughtLevel !== undefined ? { thoughtLevel: effectiveThoughtLevel } : {}),
81
88
  },
82
89
  });
83
90
  pumpRuntimeEvents(turn, ws, config, command);
@@ -94,7 +101,7 @@ export async function handlePrompt(ws, config, command) {
94
101
  evidence: { requestId: command.requestId, turnId: command.turnId, error: errorMessage(error) },
95
102
  });
96
103
  }
97
- const refreshedMetadata = await refreshedRuntimeMetadata(config, command, session, result);
104
+ const refreshedMetadata = await refreshedRuntimeMetadata(config, command, session, result, previousMetadata, effectiveModel, effectiveThoughtLevel);
98
105
  if (session.handle.backendSessionId && session.handle.backendSessionId !== previousBackendSessionId) {
99
106
  await appendAudit({
100
107
  sessionId: command.sessionId,
@@ -171,13 +178,16 @@ function pumpRuntimeEvents(turn, ws, config, command) {
171
178
  }
172
179
  })();
173
180
  }
174
- async function refreshedRuntimeMetadata(config, command, session, result) {
181
+ async function refreshedRuntimeMetadata(config, command, session, result, previousMetadata, modelMode, thoughtLevel) {
175
182
  if (!session.handle.backendSessionId)
176
183
  return undefined;
177
184
  try {
178
185
  return await encryptedSessionMetadata(config, command.sessionId, session, {
179
186
  currentHead: result.currentHead,
180
187
  emptyRuntimeSession: result.status === "failed" && !result.currentHead && !result.lastTurnId,
188
+ ...(modelMode !== undefined ? { modelMode } : {}),
189
+ ...(thoughtLevel !== undefined ? { thoughtLevel } : {}),
190
+ ...(previousMetadata?.runtimeOptions !== undefined ? { runtimeOptions: previousMetadata.runtimeOptions } : {}),
181
191
  runtimeMetadataRefreshedAt: new Date().toISOString(),
182
192
  });
183
193
  }
@@ -1,5 +1,5 @@
1
1
  import type { SessionHeadAdvanceBasis } from "../../../packages/shared/dist/index.js";
2
- import type { RuntimeHandle, RuntimeTurn } from "../../../packages/runtime/dist/index.js";
2
+ import type { RuntimeHandle, RuntimeOptions, RuntimeTurn } from "../../../packages/runtime/dist/index.js";
3
3
  export type DirectoryListResult = {
4
4
  machineId: string;
5
5
  path: string;
@@ -57,6 +57,9 @@ export type SessionMetadata = {
57
57
  providerName?: string;
58
58
  requestedRuntimeName?: string;
59
59
  permissionMode?: PermissionMode;
60
+ modelMode?: string | null;
61
+ thoughtLevel?: string | null;
62
+ runtimeOptions?: RuntimeOptions;
60
63
  importedFromRuntimeSessionId?: string;
61
64
  importedSummary?: string;
62
65
  sourceSessionId?: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.50",
3
+ "version": "0.1.0-beta.52",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -9,6 +9,7 @@ export type PendingCommandContext = {
9
9
  command?: string;
10
10
  cwd?: string;
11
11
  machineId: string;
12
+ model?: string | null;
12
13
  name?: string;
13
14
  promptHash?: string;
14
15
  recipientDeviceId?: string;
@@ -19,6 +20,7 @@ export type PendingCommandContext = {
19
20
  sourceSessionId?: string;
20
21
  targetSessionId?: string;
21
22
  timeoutMs?: number;
23
+ thoughtLevel?: string | null;
22
24
  turnId?: string;
23
25
  };
24
26
  export declare function machineKey(accountId: string, machineId: string): string;
@@ -169,12 +169,16 @@ export function handleControllerMessage(context, connection, message) {
169
169
  return;
170
170
  }
171
171
  rememberAcceptedRun(connection, { machineId: sessionRow.machine_id, sessionId: sessionRow.id, turnId: message.turnId });
172
+ const model = normalizeRuntimeSelectionContext(message.model);
173
+ const thoughtLevel = normalizeRuntimeSelectionContext(message.thoughtLevel);
172
174
  markCommandPending(connection.accountId, sessionRow.machine_id, message.requestId, {
173
175
  action: "session.run",
174
176
  blocksSessionTurn: true,
177
+ model,
175
178
  promptHash: message.promptHash,
176
179
  recipientDeviceId: connection.deviceId ?? undefined,
177
180
  sessionId: sessionRow.id,
181
+ thoughtLevel,
178
182
  turnId: message.turnId,
179
183
  });
180
184
  sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true, machineId: sessionRow.machine_id, sessionId: sessionRow.id, action: "session.run" });
@@ -198,6 +202,8 @@ export function handleControllerMessage(context, connection, message) {
198
202
  encryptedPrompt: message.encryptedPrompt,
199
203
  encryptedMetadata: sessionRow.encrypted_metadata ? parseJson(sessionRow.encrypted_metadata, undefined) : undefined,
200
204
  permissionMode: message.permissionMode,
205
+ model,
206
+ thoughtLevel,
201
207
  timeoutMs: message.timeoutMs,
202
208
  });
203
209
  return;
@@ -730,7 +736,15 @@ function controllerMessageCommandContext(message, targetMachineId) {
730
736
  case "controller:createSession":
731
737
  return { action: "session.create", agent: message.agent, cwd: message.cwd, machineId: message.machineId, name: message.name };
732
738
  case "controller:prompt":
733
- return { action: "session.run", machineId: targetMachineId, promptHash: message.promptHash, sessionId: message.sessionId, turnId: message.turnId };
739
+ return {
740
+ action: "session.run",
741
+ machineId: targetMachineId,
742
+ model: normalizeRuntimeSelectionContext(message.model),
743
+ promptHash: message.promptHash,
744
+ sessionId: message.sessionId,
745
+ thoughtLevel: normalizeRuntimeSelectionContext(message.thoughtLevel),
746
+ turnId: message.turnId,
747
+ };
734
748
  case "controller:cancel":
735
749
  return { action: "session.cancel", machineId: targetMachineId, sessionId: message.sessionId, turnId: message.turnId };
736
750
  case "controller:resume":
@@ -791,6 +805,8 @@ function pendingCommandMatchesMessage(pending, expected) {
791
805
  return false;
792
806
  if (!matchesDefined(pending.name, expected.name))
793
807
  return false;
808
+ if (expected.action === "session.run" && !matchesRuntimeSelection(pending.model, expected.model))
809
+ return false;
794
810
  if (!matchesDefined(pending.promptHash, expected.promptHash))
795
811
  return false;
796
812
  if (!matchesDefined(pending.runtimeSessionId, expected.runtimeSessionId))
@@ -801,6 +817,8 @@ function pendingCommandMatchesMessage(pending, expected) {
801
817
  return false;
802
818
  if (!matchesDefined(pending.targetSessionId, expected.targetSessionId))
803
819
  return false;
820
+ if (expected.action === "session.run" && !matchesRuntimeSelection(pending.thoughtLevel, expected.thoughtLevel))
821
+ return false;
804
822
  if (!matchesDefined(pending.turnId, expected.turnId))
805
823
  return false;
806
824
  if (!matchesDefined(pending.checkpoint, expected.checkpoint))
@@ -833,6 +851,8 @@ function retainedResponseMatchesCommand(previous, expected) {
833
851
  return false;
834
852
  if (!matchesDefined(responseTurnId(response), expected.turnId))
835
853
  return false;
854
+ if (expected.action === "session.run" && (expected.model !== undefined || expected.thoughtLevel !== undefined))
855
+ return false;
836
856
  if (expected.action === "machine.devExec" && !legacyDevExecResponseMatches(response, expected))
837
857
  return false;
838
858
  return true;
@@ -911,6 +931,17 @@ function responseTurnId(response) {
911
931
  function matchesDefined(actual, expected) {
912
932
  return expected === undefined || actual === expected;
913
933
  }
934
+ function matchesRuntimeSelection(actual, expected) {
935
+ return actual === expected;
936
+ }
937
+ function normalizeRuntimeSelectionContext(value) {
938
+ if (value === undefined)
939
+ return undefined;
940
+ if (value === null)
941
+ return null;
942
+ const normalized = value.trim();
943
+ return normalized.length === 0 || normalized.toLowerCase() === "default" ? null : normalized;
944
+ }
914
945
  function sendRequestIdConflict(connection, send, requestId) {
915
946
  send(connection.socket, {
916
947
  type: "server:error",
@@ -21,6 +21,38 @@ function primitiveSupport(value) {
21
21
  function historyModel(value) {
22
22
  return value === "append-only" || value === "authoritative-linear" ? value : undefined;
23
23
  }
24
+ function runtimeSelectionOption(value) {
25
+ if (!isRecord(value) || typeof value.key !== "string" || value.key.length === 0 || typeof value.label !== "string" || value.label.length === 0) {
26
+ return undefined;
27
+ }
28
+ return {
29
+ key: value.key,
30
+ label: value.label,
31
+ ...(typeof value.description === "string" || value.description === null ? { description: value.description } : {}),
32
+ };
33
+ }
34
+ function runtimeSelectionOptions(value) {
35
+ if (!Array.isArray(value))
36
+ return undefined;
37
+ const options = value.flatMap((item) => {
38
+ const option = runtimeSelectionOption(item);
39
+ return option ? [option] : [];
40
+ });
41
+ return options.length > 0 ? options : undefined;
42
+ }
43
+ function runtimeOptions(value) {
44
+ if (!isRecord(value))
45
+ return undefined;
46
+ const models = runtimeSelectionOptions(value.models);
47
+ const thoughtLevels = runtimeSelectionOptions(value.thoughtLevels);
48
+ const normalized = {
49
+ ...(models ? { models } : {}),
50
+ ...(thoughtLevels ? { thoughtLevels } : {}),
51
+ ...(typeof value.currentModel === "string" && value.currentModel.length > 0 ? { currentModel: value.currentModel } : {}),
52
+ ...(typeof value.currentThoughtLevel === "string" && value.currentThoughtLevel.length > 0 ? { currentThoughtLevel: value.currentThoughtLevel } : {}),
53
+ };
54
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
55
+ }
24
56
  export function normalizeMachineCapabilities(value) {
25
57
  const record = isRecord(value) ? value : {};
26
58
  const agents = Array.isArray(record.agents)
@@ -32,6 +64,7 @@ export function normalizeMachineCapabilities(value) {
32
64
  label: agent.label,
33
65
  ...(typeof agent.command === "string" ? { command: agent.command } : {}),
34
66
  ...(historyModel(agent.historyModel) ? { historyModel: historyModel(agent.historyModel) } : {}),
67
+ ...(runtimeOptions(agent.runtimeOptions) ? { runtimeOptions: runtimeOptions(agent.runtimeOptions) } : {}),
35
68
  ...(isRecord(agent.sessionPrimitives)
36
69
  ? {
37
70
  sessionPrimitives: {
@@ -285,6 +285,7 @@ export function createRelayContext(options) {
285
285
  ...(pending.command !== undefined ? { command: pending.command } : {}),
286
286
  ...(pending.cwd !== undefined ? { cwd: pending.cwd } : {}),
287
287
  machineId: pending.machineId,
288
+ ...(pending.model !== undefined ? { model: pending.model } : {}),
288
289
  ...(pending.name !== undefined ? { name: pending.name } : {}),
289
290
  ...(pending.promptHash !== undefined ? { promptHash: pending.promptHash } : {}),
290
291
  ...(pending.runtimeSessionId !== undefined ? { runtimeSessionId: pending.runtimeSessionId } : {}),
@@ -293,6 +294,7 @@ export function createRelayContext(options) {
293
294
  ...(pending.sourceSessionId !== undefined ? { sourceSessionId: pending.sourceSessionId } : {}),
294
295
  ...(pending.targetSessionId !== undefined ? { targetSessionId: pending.targetSessionId } : {}),
295
296
  ...(pending.timeoutMs !== undefined ? { timeoutMs: pending.timeoutMs } : {}),
297
+ ...(pending.thoughtLevel !== undefined ? { thoughtLevel: pending.thoughtLevel } : {}),
296
298
  ...(pending.turnId !== undefined ? { turnId: pending.turnId } : {}),
297
299
  });
298
300
  }
@@ -319,6 +321,7 @@ export function createRelayContext(options) {
319
321
  ...(typeof parsed.command === "string" ? { command: parsed.command } : {}),
320
322
  ...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
321
323
  machineId: parsed.machineId,
324
+ ...(runtimeSelectionContext(parsed.model) !== undefined ? { model: runtimeSelectionContext(parsed.model) } : {}),
322
325
  ...(typeof parsed.name === "string" ? { name: parsed.name } : {}),
323
326
  ...(typeof parsed.promptHash === "string" ? { promptHash: parsed.promptHash } : {}),
324
327
  requestId: "",
@@ -328,6 +331,7 @@ export function createRelayContext(options) {
328
331
  ...(typeof parsed.sourceSessionId === "string" ? { sourceSessionId: parsed.sourceSessionId } : {}),
329
332
  ...(typeof parsed.targetSessionId === "string" ? { targetSessionId: parsed.targetSessionId } : {}),
330
333
  ...(typeof parsed.timeoutMs === "number" ? { timeoutMs: parsed.timeoutMs } : {}),
334
+ ...(runtimeSelectionContext(parsed.thoughtLevel) !== undefined ? { thoughtLevel: runtimeSelectionContext(parsed.thoughtLevel) } : {}),
331
335
  ...(typeof parsed.turnId === "string" ? { turnId: parsed.turnId } : {}),
332
336
  };
333
337
  }
@@ -335,6 +339,11 @@ export function createRelayContext(options) {
335
339
  return undefined;
336
340
  }
337
341
  }
342
+ function runtimeSelectionContext(value) {
343
+ if (value === null)
344
+ return null;
345
+ return typeof value === "string" ? value : undefined;
346
+ }
338
347
  function rememberDirectCommandResponse(accountId, machineId, requestId, response) {
339
348
  const pending = pendingWithMachine(accountId, machineId, requestId);
340
349
  const recipientDeviceId = pending?.recipientDeviceId;