teapot-coding-agent 0.37.78 → 0.37.80

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.
@@ -1807,6 +1807,15 @@ export class Agent {
1807
1807
  await this.log.append("usage", this.currentSession, this.currentBranch, res.usage);
1808
1808
  }
1809
1809
  const m = res.message;
1810
+ /**
1811
+ * Carry the thinking on the message itself, not only in the log.
1812
+ *
1813
+ * `res.reasoning` was written to `chat.jsonl` and then dropped: `this.messages.push(m)` stored a
1814
+ * message with no reasoning, so the NEXT request sent back an assistant turn with its thinking
1815
+ * missing. Providers that require it (Beam, #187) answered 400; the rest simply lost the thread.
1816
+ */
1817
+ if (res.reasoning && !m.reasoning)
1818
+ m.reasoning = res.reasoning;
1810
1819
  const isPlaceholder = m.content === "(tool call)" || m.content === "(no content)";
1811
1820
  await this.log.append("message", this.currentSession, this.currentBranch, {
1812
1821
  role: "assistant",
@@ -2334,6 +2343,9 @@ export class Agent {
2334
2343
  resetToolCatalog(this.toolCtx);
2335
2344
  const res = await this.llmCall(this.buildMessages(), allToolSpecs());
2336
2345
  const calls = res.message.tool_calls ?? [];
2346
+ // same rule as the main loop: the thinking belongs to the message it came with (#187)
2347
+ if (res.reasoning && !res.message.reasoning)
2348
+ res.message.reasoning = res.reasoning;
2337
2349
  this.messages.push(res.message);
2338
2350
  await this.log.append("message", this.currentSession, this.currentBranch, {
2339
2351
  role: "assistant",
@@ -2993,7 +3005,13 @@ async function rebuildMessagesFrom(list, imagesDir) {
2993
3005
  const hasCalls = Array.isArray(d.toolCalls) && d.toolCalls.length > 0;
2994
3006
  if (role === "assistant" && !content && !hasCalls)
2995
3007
  continue;
2996
- const m = { role, content };
3008
+ const m = {
3009
+ role,
3010
+ content,
3011
+ ...(role === "assistant" && typeof d.reasoning === "string" && d.reasoning
3012
+ ? { reasoning: d.reasoning }
3013
+ : {}),
3014
+ };
2997
3015
  if (hasCalls) {
2998
3016
  m.tool_calls = d.toolCalls.map((c) => ({
2999
3017
  id: c.id,
package/dist/agent/llm.js CHANGED
@@ -188,7 +188,11 @@ function issuedBefore(messages, result) {
188
188
  }
189
189
  return false;
190
190
  }
191
- function sanitize(messages) {
191
+ /** the field name this provider uses for a turn's thinking */
192
+ export function reasoningSpelling(supported) {
193
+ return supported?.includes("reasoning") ? "plain" : "content";
194
+ }
195
+ function sanitize(messages, supported) {
192
196
  return repairToolPairing(messages).map((m) => {
193
197
  if (m.content_parts?.length) {
194
198
  const parts = m.content_parts.map((p) => p.type === "image_url"
@@ -208,7 +212,7 @@ function sanitize(messages) {
208
212
  // `content_parts` is OUR field, carrying the parts into `content`; sending it upstream is an
209
213
  // unknown property (a strict server rejects the request), so it is dropped here.
210
214
  const { content_parts: _parts, ...rest } = m;
211
- return { ...rest, content: parts };
215
+ return withReasoning({ ...rest, content: parts }, reasoningSpelling(supported));
212
216
  }
213
217
  if ((m.role === "user" || m.role === "assistant") && !m.content) {
214
218
  return { ...m, content: m.tool_calls?.length ? "(tool call)" : "(no content)" };
@@ -216,9 +220,51 @@ function sanitize(messages) {
216
220
  if (m.role === "tool" && typeof m.content !== "string") {
217
221
  return { ...m, content: String(m.content ?? "(no output)") };
218
222
  }
219
- return { ...m };
223
+ return withReasoning({ ...m }, reasoningSpelling(supported));
220
224
  });
221
225
  }
226
+ /**
227
+ * Send an assistant turn's thinking back as `reasoning_content`.
228
+ *
229
+ * WHY THIS EXISTS AT ALL: providers disagree on the spelling. MEASURED (commit 880d054, #153):
230
+ *
231
+ * OpenRouter (nemotron) -> `reasoning`
232
+ * commandcode (deepseek) -> `reasoning_content`
233
+ *
234
+ * so the READ side accepts either (`reasoningTextOf`). The WRITE side cannot guess: OpenRouter rejects
235
+ * a field it does not know, and a reasoning model reached through Command Code REQUIRES
236
+ * `reasoning_content` on EVERY earlier assistant turn (#187).
237
+ *
238
+ * WHY THE EMPTY-STRING CASE: a turn whose thinking the provider never returned (or that was compacted
239
+ * away) still has to carry the field, because these providers check PRESENCE across the whole history,
240
+ * not just the turns that had thinking. MEASURED — this is what `ClawRouter` does after hitting the
241
+ * same wall: patch EVERY assistant message with `reasoning_content: ""` when it is absent, because
242
+ * "reasoning models require it on every assistant turn in history, not just tool-call turns".
243
+ *
244
+ * Only assistant messages get it: a user prompt or a tool result has no thinking, and a stray field
245
+ * there is noise.
246
+ */
247
+ function withReasoning(m, spell) {
248
+ if (m.role !== "assistant")
249
+ return m;
250
+ const r = typeof m.reasoning === "string" ? m.reasoning : undefined;
251
+ // `reasoning` is OUR field; it travels under the provider's spelling, so the original is dropped
252
+ const { reasoning: _r, ...rest } = m;
253
+ /**
254
+ * WHICH SPELLING is decided by what the provider ADVERTISES, and that is measurable.
255
+ *
256
+ * MEASURED on `/models`:
257
+ * OpenRouter -> `supported_parameters` includes `reasoning` and `include_reasoning`
258
+ * CommandCode -> no `supported_parameters` at all
259
+ *
260
+ * `reasoning` is what OpenRouter understands (and it rejects a key it does not know), while Command
261
+ * Code / DeepSeek requires `reasoning_content`. So: advertised `reasoning` → use `reasoning`; silence
262
+ * → `reasoning_content`, the DeepSeek-family spelling that is far more common among OpenAI-compatible
263
+ * servers.
264
+ */
265
+ const key = spell === "plain" ? "reasoning" : "reasoning_content";
266
+ return { ...rest, [key]: r ?? "" };
267
+ }
222
268
  function reasoningTextOf(m) {
223
269
  for (const key of ["reasoning", "reasoning_content"]) {
224
270
  const v = m?.[key];
@@ -270,7 +316,7 @@ export async function chat(cfg, messages, tools, signal, onDelta) {
270
316
  try {
271
317
  const { value: res } = await withKeyRotation(cfg, (c) => client(c).chat.completions.create({
272
318
  model: c.model,
273
- messages: sanitize(messages),
319
+ messages: sanitize(messages, cfg.supportedParameters),
274
320
  ...(tools.length ? { tools } : {}),
275
321
  ...effortForRequest(c),
276
322
  ...cacheControlForRequest(c),
@@ -346,7 +392,7 @@ export async function chatStream(cfg, messages, tools, signal, onDelta) {
346
392
  try {
347
393
  const { value: stream } = await withKeyRotation(cfg, (c) => client(c).chat.completions.create({
348
394
  model: c.model,
349
- messages: sanitize(messages),
395
+ messages: sanitize(messages, cfg.supportedParameters),
350
396
  ...(tools.length ? { tools } : {}),
351
397
  ...effortForRequest(c),
352
398
  ...cacheControlForRequest(c),
package/dist/master.js CHANGED
@@ -664,6 +664,8 @@ export class Master {
664
664
  ...(ac.chatFn ? { chatFn: ac.chatFn } : {}),
665
665
  ...(ac.reasoningEffort ? { reasoningEffort: ac.reasoningEffort } : {}),
666
666
  ...(modelMeta?.supportedEfforts?.length ? { supportedEfforts: modelMeta.supportedEfforts } : {}),
667
+ // decides the spelling of a turn's thinking on the wire (#187)
668
+ ...(modelMeta?.supportedParameters?.length ? { supportedParameters: modelMeta.supportedParameters } : {}),
667
669
  });
668
670
  void fetchModelList(llm.baseUrl, llm.apiKey)
669
671
  .then((list) => agent.setModelPricing(list.find((m) => m.id === llm.model)?.pricing))
@@ -1457,6 +1459,7 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
1457
1459
  cfgEntry.reasoningEffort = opts.opts.reasoningEffort || undefined;
1458
1460
  }
1459
1461
  opts.opts.supportedEfforts = meta?.supportedEfforts;
1462
+ opts.opts.supportedParameters = meta?.supportedParameters;
1460
1463
  if (!llm.model)
1461
1464
  throw new Error("no model resolved (pass model or set one on the provider)");
1462
1465
  if (!llm.baseUrl)
@@ -231,6 +231,7 @@ async function fetchProviderModels(baseUrl, apiKey) {
231
231
  ...(ctx !== undefined ? { contextLength: ctx } : {}),
232
232
  ...(input ? { modalities: { input, output: output ?? ["text"] } } : {}),
233
233
  ...(params?.some((p) => p === "reasoning" || p === "reasoning_effort") ? { reasoning: true } : {}),
234
+ ...(params ? { supportedParameters: params } : {}),
234
235
  });
235
236
  }
236
237
  return out.length ? out : null;
@@ -251,6 +252,9 @@ function mergeMeta(own, db) {
251
252
  * effort LEVELS, only that reasoning exists. `own.reasoning` still wins for the yes/no question.
252
253
  */
253
254
  ...(db.supportedEfforts ? { supportedEfforts: db.supportedEfforts } : {}),
255
+ ...(own.supportedParameters ?? db.supportedParameters
256
+ ? { supportedParameters: own.supportedParameters ?? db.supportedParameters }
257
+ : {}),
254
258
  ...(own.reasoning ?? db.reasoning ? { reasoning: own.reasoning ?? db.reasoning } : {}),
255
259
  ...(db.pricing ? { pricing: db.pricing } : {}),
256
260
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.37.78",
3
+ "version": "0.37.80",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",