@agjs/tsforge 0.1.18 → 0.1.19

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agjs/tsforge",
3
3
  "type": "module",
4
- "version": "0.1.18",
4
+ "version": "0.1.19",
5
5
  "license": "MIT",
6
6
  "description": "TypeScript coding harness with a deterministic gate, stack-aware guardrails, and stream-level correction.",
7
7
  "repository": {
@@ -1,5 +1,18 @@
1
1
  import { resolve, dirname, basename, join } from "node:path";
2
- import { chromium, type Page } from "playwright";
2
+ // `playwright` is an OPTIONAL peer: bundling it (+ a browser binary) into every
3
+ // install is too heavy, so the import is dynamic and the render-check skips when
4
+ // it's absent. The type-only import is erased at runtime, so it can't crash a
5
+ // playwright-less install.
6
+ import type { Page, chromium as Chromium } from "playwright";
7
+
8
+ /** Load playwright's chromium lazily; null when it isn't installed. */
9
+ async function loadChromium(): Promise<typeof Chromium | null> {
10
+ try {
11
+ return (await import("playwright")).chromium;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
3
16
 
4
17
  /**
5
18
  * The browser oracle — renders a built web page in headless chromium and reports
@@ -50,12 +63,27 @@ export interface IRenderResult {
50
63
  ok: boolean;
51
64
  /** Human-readable failures (console errors, page errors, missing content). */
52
65
  errors: string[];
66
+ /** True when the check was skipped because playwright isn't installed. */
67
+ skipped?: boolean;
53
68
  }
54
69
 
55
70
  export async function renderCheck(
56
71
  opts: IRenderOptions
57
72
  ): Promise<IRenderResult> {
58
73
  const errors: string[] = [];
74
+ const chromium = await loadChromium();
75
+
76
+ // No playwright → skip the render check rather than fail the gate. The build
77
+ // still ran tsc/eslint/build/stub-check; the browser smoke is an enhancement.
78
+ if (chromium === null) {
79
+ process.stderr.write(
80
+ "browser render-check skipped: playwright not installed " +
81
+ "(run `bunx playwright install chromium` to enable it)\n"
82
+ );
83
+
84
+ return { ok: true, errors: [], skipped: true };
85
+ }
86
+
59
87
  const browser = await chromium.launch({ args: ["--no-sandbox"] });
60
88
 
61
89
  try {
@@ -8,6 +8,10 @@ export interface IChatMessage {
8
8
  toolCalls?: IToolCall[];
9
9
  /** Tool messages only: the id of the call this message is the result of. */
10
10
  toolCallId?: string;
11
+ /** Assistant only: the model's chain-of-thought. DeepSeek's thinking mode
12
+ * REQUIRES the prior turn's `reasoning_content` to be replayed, so it's kept
13
+ * on the message and re-sent (for the deepseek reasoning style). */
14
+ reasoningContent?: string;
11
15
  }
12
16
 
13
17
  /** A parsed tool call from the model (name + decoded JSON arguments). */
@@ -29,6 +33,10 @@ export interface ITokenUsage {
29
33
  export interface IModelResponse {
30
34
  content: string;
31
35
  toolCalls: IToolCall[];
36
+ /** The model's chain-of-thought (`reasoning`/`reasoning_content`), when it
37
+ * produced any. Stored on the assistant message for providers (DeepSeek) that
38
+ * require it replayed on the next turn. */
39
+ reasoning?: string;
32
40
  /** Server-reported token usage for this call, when available. `promptTokens`
33
41
  * is the full context the model just saw — what auto-compaction will watch. */
34
42
  usage?: ITokenUsage;
@@ -99,9 +99,13 @@ export function buildRequestBody(
99
99
  const omitTemperature =
100
100
  style(cfg) === "openai" || opts.temperature === undefined;
101
101
 
102
+ // DeepSeek's thinking mode requires each prior assistant turn's
103
+ // `reasoning_content` replayed; other providers don't want it.
104
+ const includeReasoning = style(cfg) === "deepseek";
105
+
102
106
  return {
103
107
  model: cfg.model,
104
- messages: messages.map(toWire),
108
+ messages: messages.map((m) => toWire(m, includeReasoning)),
105
109
  ...tokenCapField(cfg),
106
110
  ...(omitTemperature ? {} : { temperature: opts.temperature }),
107
111
  ...(cfg.repetitionPenalty === undefined
@@ -35,6 +35,7 @@ export async function streamResponse(
35
35
  calls: new Map(),
36
36
  guard: new StreamGuard(),
37
37
  content: "",
38
+ reasoning: "",
38
39
  ttsr: ttsrManager,
39
40
  ttsrFired: null,
40
41
  };
@@ -88,6 +89,7 @@ interface IStreamAcc {
88
89
  calls: Map<number, IStreamingCall>;
89
90
  guard: StreamGuard;
90
91
  content: string;
92
+ reasoning: string;
91
93
  usage?: ITokenUsage;
92
94
  ttsr?: ITtsrWatcher;
93
95
  ttsrFired: { readonly name: string; readonly guidance: string } | null;
@@ -137,6 +139,7 @@ function consumeLines(
137
139
  // less, not by hiding it from the log.)
138
140
  if (delta.reasoning !== undefined && delta.reasoning.length > 0) {
139
141
  onToken(delta.reasoning, "reasoning");
142
+ acc.reasoning += delta.reasoning;
140
143
 
141
144
  if (acc.guard.observe(delta.reasoning, "reasoning")) {
142
145
  return true;
@@ -163,6 +166,8 @@ function consumeLines(
163
166
 
164
167
  function assemble(acc: IStreamAcc, degenerated: boolean): IModelResponse {
165
168
  const usage = acc.usage === undefined ? {} : { usage: acc.usage };
169
+ const reasoning =
170
+ acc.reasoning.length > 0 ? { reasoning: acc.reasoning } : {};
166
171
  const toolCalls: IToolCall[] = [...acc.calls.values()].map((c) => ({
167
172
  id: c.id,
168
173
  name: c.name,
@@ -181,8 +186,21 @@ function assemble(acc: IStreamAcc, degenerated: boolean): IModelResponse {
181
186
 
182
187
  if (toolCalls.length > 0) {
183
188
  return degenerated
184
- ? { content: acc.content, toolCalls, degenerated, ...ttsrFired, ...usage }
185
- : { content: acc.content, toolCalls, ...ttsrFired, ...usage };
189
+ ? {
190
+ content: acc.content,
191
+ toolCalls,
192
+ degenerated,
193
+ ...reasoning,
194
+ ...ttsrFired,
195
+ ...usage,
196
+ }
197
+ : {
198
+ content: acc.content,
199
+ toolCalls,
200
+ ...reasoning,
201
+ ...ttsrFired,
202
+ ...usage,
203
+ };
186
204
  }
187
205
 
188
206
  const salvaged = salvageToolCalls(acc.content);
@@ -192,6 +210,7 @@ function assemble(acc: IStreamAcc, degenerated: boolean): IModelResponse {
192
210
  toolCalls: salvaged,
193
211
  salvaged: salvaged.length,
194
212
  ...(degenerated ? { degenerated } : {}),
213
+ ...reasoning,
195
214
  ...ttsrFired,
196
215
  ...usage,
197
216
  };
Binary file
package/src/loop/run.ts CHANGED
@@ -366,6 +366,9 @@ export async function runTask(
366
366
  role: "assistant",
367
367
  content: res.content,
368
368
  toolCalls: res.toolCalls,
369
+ ...(res.reasoning === undefined
370
+ ? {}
371
+ : { reasoningContent: res.reasoning }),
369
372
  });
370
373
 
371
374
  // Every model call advances cooldown accounting — including interrupted
@@ -144,6 +144,17 @@ export interface ISendOptions {
144
144
 
145
145
  const SESSION_ID = "session";
146
146
 
147
+ /** Build the assistant history message, carrying `reasoningContent` when the
148
+ * model produced it (DeepSeek's thinking mode requires it replayed). */
149
+ function assistantMessage(res: IModelResponse): IChatMessage {
150
+ return {
151
+ role: "assistant",
152
+ content: res.content,
153
+ toolCalls: res.toolCalls,
154
+ ...(res.reasoning === undefined ? {} : { reasoningContent: res.reasoning }),
155
+ };
156
+ }
157
+
147
158
  /** Default share of the context window that triggers auto-compaction. */
148
159
  const AUTO_COMPACT_AT = 0.8;
149
160
 
@@ -1054,11 +1065,7 @@ export class Session {
1054
1065
  });
1055
1066
  }
1056
1067
 
1057
- ctx.messages.push({
1058
- role: "assistant",
1059
- content: res.content,
1060
- toolCalls: res.toolCalls,
1061
- });
1068
+ ctx.messages.push(assistantMessage(res));
1062
1069
 
1063
1070
  if (res.salvaged !== undefined && res.salvaged > 0) {
1064
1071
  report({