@agjs/tsforge 0.1.17 → 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 +1 -1
- package/src/browser/oracle.ts +29 -1
- package/src/inference/inference.types.ts +8 -0
- package/src/inference/request.ts +5 -1
- package/src/inference/stream.ts +21 -2
- package/src/inference/wire.ts +0 -0
- package/src/loop/feedback/rule-docs.ts +15 -0
- package/src/loop/run.ts +3 -0
- package/src/loop/session.ts +12 -5
package/package.json
CHANGED
package/src/browser/oracle.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { resolve, dirname, basename, join } from "node:path";
|
|
2
|
-
|
|
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;
|
package/src/inference/request.ts
CHANGED
|
@@ -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
|
package/src/inference/stream.ts
CHANGED
|
@@ -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
|
-
? {
|
|
185
|
-
|
|
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
|
};
|
package/src/inference/wire.ts
CHANGED
|
Binary file
|
|
@@ -139,6 +139,21 @@ const RULE_DOCS: Record<string, IRuleDoc> = {
|
|
|
139
139
|
bad: "items.map((it, i) => <li key={i}>{it.text}</li>)",
|
|
140
140
|
good: "items.map((it) => <li key={it.id}>{it.text}</li>)",
|
|
141
141
|
},
|
|
142
|
+
"tsforge/no-jsx-computation": {
|
|
143
|
+
what: "No `.map()`/`.filter()`/arithmetic/chained logic inside JSX `{…}` — extract to a hook or pre-prep variable first.",
|
|
144
|
+
bad: "<ul>{items.filter((i) => i.visible).map((i) => <li key={i.id}>{i.label}</li>)}</ul>",
|
|
145
|
+
good: "const rows = useMemo(() => items.filter(...).map(...), [items]); return <ul>{rows}</ul>;",
|
|
146
|
+
},
|
|
147
|
+
"tsforge/no-state-in-component-body": {
|
|
148
|
+
what: "State hooks (`useState`, `useEffect`, `useMemo`, …) belong in `Component.hooks.ts`, not in the `.tsx` component body.",
|
|
149
|
+
bad: "export function Button() { const [open, setOpen] = useState(false); return <button />; }",
|
|
150
|
+
good: "export function useButton() { const [open, setOpen] = useState(false); return { open }; }",
|
|
151
|
+
},
|
|
152
|
+
"tsforge/no-inline-jsx-functions": {
|
|
153
|
+
what: "No inline arrow/function expressions in JSX attributes — bind handlers in the hook and pass a reference.",
|
|
154
|
+
bad: "<button onClick={() => doThing(id)} />",
|
|
155
|
+
good: "const onClickRow = useCallback(() => doThing(id), [id]); <button onClick={onClickRow} />",
|
|
156
|
+
},
|
|
142
157
|
};
|
|
143
158
|
|
|
144
159
|
/**
|
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
|
package/src/loop/session.ts
CHANGED
|
@@ -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({
|