archymedes-cli 2.2.0 → 2.5.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/README.md CHANGED
@@ -312,13 +312,23 @@ The startup geometry makes a brief rotation on roomy interactive terminals. Set
312
312
 
313
313
  ## Fixed workspace
314
314
 
315
- Start with `archymedes --layout fixed` for a persistent mode bar, an anchored composer, and menus
316
- that open inside the transcript area. `/layout` switches layouts during a session;
317
- `ARCHYMEDES_LAYOUT=fixed` makes the fixed workspace your launch preference.
318
-
319
- Use `/mode` to choose plan, build, auto, or defender with the arrow keys. Page Up / Page Down
320
- read retained transcript history; Escape returns to live output. Home and End still edit the
321
- composer. Each tab retains up to 5,000 output lines. Resize reflows the live transcript.
315
+ Interactive sessions open in the fixed workspace: a persistent mode bar, an anchored composer, and
316
+ menus that open inside the transcript area. `/layout` switches layouts during a session;
317
+ `archymedes --layout scrollback` or `ARCHYMEDES_LAYOUT=scrollback` keeps the plain terminal log.
318
+
319
+ Use `/mode` to choose plan, build, auto, or defender with the arrow keys. To read history, use the
320
+ mouse wheel or Page Up / Page Down. Alt+Up / Alt+Down (or Ctrl) move one line, and Ctrl+Home jumps
321
+ to the top. Escape, Ctrl+End, or scrolling back to the bottom returns to live output. History keeps
322
+ its colours, a scrollbar shows your position, and the header counts output that arrived while you
323
+ were reading. Arrow keys, Home and End still edit the composer. Wheel scrolling uses mouse
324
+ reporting, so hold Shift to select text, or set `ARCHYMEDES_MOUSE=0` to keep the terminal's own
325
+ selection. Each tab retains up to 5,000 output lines, and resizing reflows the transcript.
326
+
327
+ `/find <text>` searches the tab's history and jumps to the first match; `/find` again moves to the
328
+ next one, `/find prev` to the previous, and `/find off` or Escape returns to live output. `/pager`
329
+ opens the whole transcript in `$PAGER` (`less -R` by default). `/cat image.png` draws a PNG in the
330
+ transcript with coloured half-blocks; in the scrollback layout, `ARCHYMEDES_IMAGES=kitty` uses Kitty
331
+ graphics for real pixels, and `/clear` removes them.
322
332
 
323
333
  The opening geometry rotates while the workspace is idle and settles as soon as you type.
324
334
  `ARCHYMEDES_NO_MOTION=1` disables this motion and menu focus animation. `--ascii`, `NO_COLOR`,
@@ -8530,10 +8530,82 @@ function toWireMessage(message) {
8530
8530
  tool_calls: message.toolCalls.map((call) => ({ id: call.id, type: "function", function: { name: call.name, arguments: JSON.stringify(call.arguments) } }))
8531
8531
  };
8532
8532
  }
8533
+ if (message.role === "user" && "images" in message && message.images?.length) {
8534
+ return {
8535
+ role: "user",
8536
+ content: [
8537
+ { type: "text", text: message.content },
8538
+ ...message.images.map((image) => ({ type: "image_url", image_url: { url: `data:${image.mediaType};base64,${image.data}` } }))
8539
+ ]
8540
+ };
8541
+ }
8533
8542
  return { role: message.role, content: message.content };
8534
8543
  }
8535
- function toWireMessages(messages) {
8536
- return messages.map(toWireMessage);
8544
+ function toWireMessages(messages, options = {}) {
8545
+ const vision = options.vision ?? true;
8546
+ const wire = [];
8547
+ let pending = [];
8548
+ const flush = () => {
8549
+ if (pending.length === 0)
8550
+ return;
8551
+ wire.push({
8552
+ role: "user",
8553
+ content: [
8554
+ { type: "text", text: `Image${pending.reduce((sum, entry) => sum + entry.images.length, 0) === 1 ? "" : "s"} returned by the ${[...new Set(pending.map((entry) => entry.name))].join(", ")} tool call${pending.length === 1 ? "" : "s"} above: ${pending.flatMap((entry) => entry.images.map((image) => image.path)).join(", ")}` },
8555
+ ...pending.flatMap((entry) => entry.images.map((image) => ({ type: "image_url", image_url: { url: `data:${image.mediaType};base64,${image.data}` } })))
8556
+ ]
8557
+ });
8558
+ pending = [];
8559
+ };
8560
+ for (const message of messages) {
8561
+ if (message.role !== "tool")
8562
+ flush();
8563
+ if (message.role === "tool" && message.images?.length) {
8564
+ if (vision) {
8565
+ wire.push(toWireMessage(message));
8566
+ pending.push({ name: message.name, images: message.images });
8567
+ } else {
8568
+ const note = `[${message.images.length === 1 ? "image" : "images"} not shown: this model cannot view images (${message.images.map((image) => image.path).join(", ")})]`;
8569
+ wire.push(toWireMessage({ ...message, content: `${message.content}
8570
+ ${note}` }));
8571
+ }
8572
+ continue;
8573
+ }
8574
+ wire.push(toWireMessage(message));
8575
+ }
8576
+ flush();
8577
+ return wire;
8578
+ }
8579
+ var EPHEMERAL = { type: "ephemeral" };
8580
+ function usesOpenRouterCacheControl(model) {
8581
+ return model.startsWith("anthropic/") || model.startsWith("google/");
8582
+ }
8583
+ function isOpenRouterBaseUrl(baseURL) {
8584
+ if (!baseURL)
8585
+ return false;
8586
+ try {
8587
+ return new URL(baseURL).hostname.endsWith("openrouter.ai");
8588
+ } catch {
8589
+ return false;
8590
+ }
8591
+ }
8592
+ function withPartBreakpoint(message) {
8593
+ if (typeof message.content === "string") {
8594
+ return message.content ? { ...message, content: [{ type: "text", text: message.content, cache_control: EPHEMERAL }] } : message;
8595
+ }
8596
+ if (!Array.isArray(message.content))
8597
+ return message;
8598
+ const parts = message.content;
8599
+ const last = parts.map((part) => part.type).lastIndexOf("text");
8600
+ if (last < 0)
8601
+ return message;
8602
+ return { ...message, content: parts.map((part, index) => index === last ? { ...part, cache_control: EPHEMERAL } : part) };
8603
+ }
8604
+ function withOpenRouterCacheControl(messages, model) {
8605
+ if (!usesOpenRouterCacheControl(model))
8606
+ return messages;
8607
+ const lastUser = messages.map((message) => message.role).lastIndexOf("user");
8608
+ return messages.map((message, index) => message.role === "system" || index === lastUser ? withPartBreakpoint(message) : message);
8537
8609
  }
8538
8610
  function parseArguments(value) {
8539
8611
  try {
@@ -8584,7 +8656,7 @@ function turnFromChatResponse(response) {
8584
8656
  ...routingReceipt ? { routingReceipt } : {}
8585
8657
  };
8586
8658
  }
8587
- async function collectChatStream(stream, onTextDelta) {
8659
+ async function collectChatStream(stream, onTextDelta, onOutputProgress) {
8588
8660
  let id = "";
8589
8661
  let model = "";
8590
8662
  let content = "";
@@ -8606,10 +8678,15 @@ async function collectChatStream(stream, onTextDelta) {
8606
8678
  finishReason = choice.finish_reason;
8607
8679
  if (choice.delta?.refusal)
8608
8680
  refusal += choice.delta.refusal;
8681
+ if (choice.delta?.reasoning || choice.delta?.reasoning_content)
8682
+ onOutputProgress?.("reasoning");
8609
8683
  if (choice.delta?.content) {
8610
8684
  content += choice.delta.content;
8685
+ onOutputProgress?.("text");
8611
8686
  onTextDelta?.(choice.delta.content);
8612
8687
  }
8688
+ if (choice.delta?.tool_calls?.length)
8689
+ onOutputProgress?.("tool_call");
8613
8690
  for (const fragment of choice.delta?.tool_calls ?? []) {
8614
8691
  const existing = calls.get(fragment.index) ?? { id: "", name: "", args: "" };
8615
8692
  calls.set(fragment.index, {
@@ -8637,6 +8714,7 @@ function exactCapabilities(models, contextWindow, maxOutputTokens, supportsEffor
8637
8714
  return models.map((prefix) => ({ prefix, match: "exact", capabilities: { contextWindow, maxOutputTokens, supportsEffort } }));
8638
8715
  }
8639
8716
  var KNOWN_CAPABILITIES = [
8717
+ { prefix: "openrouter/free", match: "exact", capabilities: { contextWindow: 32768, maxOutputTokens: 4096, supportsEffort: false } },
8640
8718
  { prefix: "auto", match: "exact", capabilities: { ...CONSERVATIVE_CAPABILITIES } },
8641
8719
  { prefix: "gemini-2.5-pro", match: "prefix", capabilities: { contextWindow: 1048576, maxOutputTokens: 65536, supportsEffort: true } },
8642
8720
  { prefix: "gemini-2.5-flash", match: "prefix", capabilities: { contextWindow: 1048576, maxOutputTokens: 65536, supportsEffort: true } },
@@ -8687,6 +8765,142 @@ function capabilitiesFor(modelId) {
8687
8765
  var DEFAULT_OUTPUT_CEILING = 64000;
8688
8766
  var PROTOCOL_MAX_OUTPUT_TOKENS = 128000;
8689
8767
 
8768
+ // packages/core/src/providers/stream-deadline.ts
8769
+ var DEFAULT_FIRST_BYTE_TIMEOUT_MS = 300000;
8770
+ var DEFAULT_IDLE_TIMEOUT_MS = 90000;
8771
+ var DEFAULT_TOTAL_TIMEOUT_MS = 30 * 60000;
8772
+
8773
+ class StreamTimeoutError extends Error {
8774
+ phase;
8775
+ timeoutMs;
8776
+ name = "TimeoutError";
8777
+ code = "ETIMEDOUT";
8778
+ constructor(phase, timeoutMs) {
8779
+ const seconds = Math.round(timeoutMs / 1000);
8780
+ super(phase === "first_byte" ? `Model request timed out: no response within ${seconds}s.` : phase === "idle" ? `Model stream timed out: no data received for ${seconds}s.` : `Model request timed out: exceeded the ${seconds}s overall limit.`);
8781
+ this.phase = phase;
8782
+ this.timeoutMs = timeoutMs;
8783
+ }
8784
+ }
8785
+ function streamTimeoutsFor(timeoutMs, overrides = {}) {
8786
+ const legacy = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
8787
+ return {
8788
+ firstByteMs: overrides.firstByteMs ?? Math.max(DEFAULT_FIRST_BYTE_TIMEOUT_MS, legacy ?? 0),
8789
+ idleMs: overrides.idleMs ?? legacy ?? DEFAULT_IDLE_TIMEOUT_MS,
8790
+ totalMs: overrides.totalMs ?? Math.max(DEFAULT_TOTAL_TIMEOUT_MS, legacy ?? 0)
8791
+ };
8792
+ }
8793
+ function createStreamDeadline(options = {}) {
8794
+ const timeouts = {
8795
+ firstByteMs: options.firstByteMs ?? DEFAULT_FIRST_BYTE_TIMEOUT_MS,
8796
+ idleMs: options.idleMs ?? DEFAULT_IDLE_TIMEOUT_MS,
8797
+ totalMs: options.totalMs ?? DEFAULT_TOTAL_TIMEOUT_MS
8798
+ };
8799
+ const controller = new AbortController;
8800
+ const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
8801
+ let timedOut;
8802
+ let started = false;
8803
+ let disposed = false;
8804
+ let phaseTimer;
8805
+ let totalTimer;
8806
+ const waiters = new Set;
8807
+ const schedule = (ms, fire) => {
8808
+ const timer = setTimeout(fire, ms);
8809
+ timer.unref?.();
8810
+ return timer;
8811
+ };
8812
+ const clear = () => {
8813
+ if (phaseTimer !== undefined)
8814
+ clearTimeout(phaseTimer);
8815
+ if (totalTimer !== undefined)
8816
+ clearTimeout(totalTimer);
8817
+ phaseTimer = totalTimer = undefined;
8818
+ };
8819
+ const expire = (phase, ms) => {
8820
+ if (disposed || timedOut || options.signal?.aborted)
8821
+ return;
8822
+ timedOut = new StreamTimeoutError(phase, ms);
8823
+ clear();
8824
+ controller.abort(timedOut);
8825
+ for (const reject of waiters)
8826
+ reject(timedOut);
8827
+ waiters.clear();
8828
+ };
8829
+ if (!options.signal?.aborted) {
8830
+ phaseTimer = schedule(timeouts.firstByteMs, () => expire("first_byte", timeouts.firstByteMs));
8831
+ totalTimer = schedule(timeouts.totalMs, () => expire("total", timeouts.totalMs));
8832
+ }
8833
+ const touch = () => {
8834
+ if (disposed || timedOut)
8835
+ return;
8836
+ started = true;
8837
+ if (phaseTimer !== undefined)
8838
+ clearTimeout(phaseTimer);
8839
+ phaseTimer = schedule(timeouts.idleMs, () => expire("idle", timeouts.idleMs));
8840
+ };
8841
+ const race = (promise) => {
8842
+ if (timedOut) {
8843
+ promise.catch(() => {
8844
+ return;
8845
+ });
8846
+ return Promise.reject(timedOut);
8847
+ }
8848
+ return new Promise((resolve, reject) => {
8849
+ const onTimeout = (error2) => {
8850
+ promise.catch(() => {
8851
+ return;
8852
+ });
8853
+ reject(error2);
8854
+ };
8855
+ waiters.add(onTimeout);
8856
+ promise.then((value) => {
8857
+ waiters.delete(onTimeout);
8858
+ resolve(value);
8859
+ }, (error2) => {
8860
+ waiters.delete(onTimeout);
8861
+ reject(error2);
8862
+ });
8863
+ });
8864
+ };
8865
+ async function* wrap(stream) {
8866
+ const iterator = stream[Symbol.asyncIterator]();
8867
+ let finished = false;
8868
+ try {
8869
+ while (true) {
8870
+ const next = await race(iterator.next());
8871
+ if (next.done) {
8872
+ finished = true;
8873
+ return;
8874
+ }
8875
+ touch();
8876
+ yield next.value;
8877
+ }
8878
+ } finally {
8879
+ if (!finished)
8880
+ Promise.resolve().then(() => iterator.return?.()).catch(() => {
8881
+ return;
8882
+ });
8883
+ }
8884
+ }
8885
+ return {
8886
+ signal,
8887
+ get timedOut() {
8888
+ return timedOut;
8889
+ },
8890
+ get started() {
8891
+ return started;
8892
+ },
8893
+ touch,
8894
+ race,
8895
+ wrap,
8896
+ dispose() {
8897
+ disposed = true;
8898
+ clear();
8899
+ waiters.clear();
8900
+ }
8901
+ };
8902
+ }
8903
+
8690
8904
  // packages/core/src/providers/openai-agent.ts
8691
8905
  function usesInklingToolContract(model) {
8692
8906
  return model === "thinkingmachines/inkling:free" || model === "thinkingmachines/inkling-small:free";
@@ -8706,7 +8920,7 @@ class OpenAIAgentTurnProvider {
8706
8920
  if (call)
8707
8921
  this.call = call;
8708
8922
  else {
8709
- const client = new OpenAI({ apiKey: options.apiKey, ...options.baseURL ? { baseURL: options.baseURL } : {}, maxRetries: 0 });
8923
+ const client = new OpenAI({ apiKey: options.apiKey, ...options.baseURL ? { baseURL: options.baseURL } : {}, ...options.defaultHeaders ? { defaultHeaders: options.defaultHeaders } : {}, maxRetries: 0 });
8710
8924
  this.call = async (body, signal) => await client.chat.completions.create(body, { signal });
8711
8925
  }
8712
8926
  }
@@ -8714,9 +8928,9 @@ class OpenAIAgentTurnProvider {
8714
8928
  if (!request.safetyIdentifier.trim())
8715
8929
  throw new Error("safetyIdentifier is required");
8716
8930
  const inkling = usesInklingToolContract(this.options.model);
8717
- const response = await this.call({
8931
+ const body = {
8718
8932
  model: this.options.model,
8719
- messages: toWireMessages(request.messages),
8933
+ messages: isOpenRouterBaseUrl(this.options.baseURL) ? withOpenRouterCacheControl(toWireMessages(request.messages), this.options.model) : toWireMessages(request.messages),
8720
8934
  ...request.tools.length > 0 ? {
8721
8935
  tools: request.tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.inputSchema } }))
8722
8936
  } : {},
@@ -8727,11 +8941,18 @@ class OpenAIAgentTurnProvider {
8727
8941
  ...request.effort && this.capabilities.supportsEffort ? { reasoning_effort: request.effort } : {},
8728
8942
  stream: true,
8729
8943
  stream_options: { include_usage: true }
8730
- }, AbortSignal.any([
8731
- AbortSignal.timeout(this.options.timeoutMs ?? 180000),
8732
- ...request.signal ? [request.signal] : []
8733
- ]));
8734
- return turnFromChatResponse(Symbol.asyncIterator in Object(response) ? await collectChatStream(response, request.onTextDelta) : response);
8944
+ };
8945
+ const deadline = createStreamDeadline({ ...streamTimeoutsFor(this.options.timeoutMs), signal: request.signal });
8946
+ try {
8947
+ const response = await deadline.race(this.call(body, deadline.signal));
8948
+ return turnFromChatResponse(Symbol.asyncIterator in Object(response) ? await collectChatStream(deadline.wrap(response), request.onTextDelta, request.onOutputProgress) : response);
8949
+ } catch (error2) {
8950
+ if (deadline.timedOut)
8951
+ throw deadline.timedOut;
8952
+ throw error2;
8953
+ } finally {
8954
+ deadline.dispose();
8955
+ }
8735
8956
  }
8736
8957
  }
8737
8958
 
@@ -8850,4 +9071,4 @@ function wanderGradeCounts(results) {
8850
9071
  return WANDER_GRADES.map((grade) => ({ grade, count: results.claims.filter((claim) => claim.grade === grade).length }));
8851
9072
  }
8852
9073
 
8853
- export { approximateInputTokens, newPartTotals, addPart, tokenEstimateFrom, affordableOutputTokensFor, affordableOutputTokens, priceActualModelUsage, modelPricesFromEnvironment, TASK_KINDS, buildTaskProfile, mergeRoutingReceipts, capabilitiesFor, DEFAULT_OUTPUT_CEILING, PROTOCOL_MAX_OUTPUT_TOKENS, makeParseableTextFormat, OpenAI, toWireMessages, turnFromChatResponse, OpenAIAgentTurnProvider, WANDER_MARKER, WANDER_LAB_FILES, isWanderObjective, isWanderRandomScheduleObjective, buildWanderScheduleObjective, pickWanderTopic, buildWanderObjective, wanderPlannerInstructions, WANDER_GRADES, parseWanderResults, wanderGradeCounts };
9074
+ export { approximateInputTokens, newPartTotals, addPart, tokenEstimateFrom, affordableOutputTokensFor, affordableOutputTokens, priceActualModelUsage, modelPricesFromEnvironment, TASK_KINDS, buildTaskProfile, mergeRoutingReceipts, capabilitiesFor, DEFAULT_OUTPUT_CEILING, PROTOCOL_MAX_OUTPUT_TOKENS, DEFAULT_TOTAL_TIMEOUT_MS, StreamTimeoutError, streamTimeoutsFor, createStreamDeadline, APIConnectionError, makeParseableTextFormat, OpenAI, toWireMessages, withOpenRouterCacheControl, turnFromChatResponse, collectChatStream, OpenAIAgentTurnProvider, WANDER_MARKER, WANDER_LAB_FILES, isWanderObjective, isWanderRandomScheduleObjective, buildWanderScheduleObjective, pickWanderTopic, buildWanderObjective, wanderPlannerInstructions, WANDER_GRADES, parseWanderResults, wanderGradeCounts };