@mjasnikovs/pi-task 0.38.13 → 0.38.14

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.38.13",
3
+ "version": "0.38.14",
4
4
  "description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "LICENSE"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsc -p tsconfig.build.json",
15
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
16
16
  "lint": "prettier --log-level warn --write 'src/**/*.ts' && eslint --fix . && tsc --noEmit && tsc -p scripts/tsconfig.json --noEmit",
17
17
  "test": "cross-env AGENT=1 bun test --isolate src/ scripts/",
18
18
  "prepublishOnly": "bun run build"
@@ -1,2 +0,0 @@
1
- export declare function isAgentIdle(): boolean;
2
- export declare function setAgentIdle(idle: boolean): void;
@@ -1,7 +0,0 @@
1
- let _isAgentIdle = true;
2
- export function isAgentIdle() {
3
- return _isAgentIdle;
4
- }
5
- export function setAgentIdle(idle) {
6
- _isAgentIdle = idle;
7
- }
@@ -1,2 +0,0 @@
1
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
- export declare function registerThinkingCompression(pi: ExtensionAPI): void;
@@ -1,118 +0,0 @@
1
- import { getConfig } from '../config/config.js';
2
- import { collectCompressible, MIN_THINKING_CHARS, rebuildWithCompressed } from './rewrite.js';
3
- /** Hard cap so a stuck model request can never wedge a turn. */
4
- const REQUEST_TIMEOUT_MS = 120_000;
5
- const STATUS_KEY = 'pi-task-thinking';
6
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
- const PROMPT = 'Compress this reasoning. Keep every decision/conclusion/constraint/fact relied on later. '
8
- + 'Drop restated questions, false starts, self-talk. Output only the compressed reasoning. /no_think';
9
- async function compressOne(text, model, auth) {
10
- const headers = { 'Content-Type': 'application/json', ...auth.headers };
11
- if (auth.apiKey)
12
- headers.Authorization = `Bearer ${auth.apiKey}`;
13
- const res = await fetch(`${model.baseUrl}/chat/completions`, {
14
- method: 'POST',
15
- headers,
16
- body: JSON.stringify({
17
- model: model.id,
18
- messages: [{ role: 'user', content: `${PROMPT}\n\n---\n\n${text}` }],
19
- temperature: 0,
20
- stream: false
21
- }),
22
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
23
- });
24
- if (!res.ok)
25
- throw new Error(`compress HTTP ${res.status}`);
26
- const data = (await res.json());
27
- const raw = data.choices?.[0]?.message?.content ?? '';
28
- return raw.replaceAll('<think>', '').replaceAll('</think>', '').trim();
29
- }
30
- /** Animated footer loader. Safe in any mode — `setStatus` is a no-op outside the
31
- * TUI. Each tick reports which block is compressing and its size. */
32
- class Loader {
33
- ui;
34
- timer = null;
35
- frame = 0;
36
- constructor(ui) {
37
- this.ui = ui;
38
- }
39
- start(label) {
40
- this.stop();
41
- const tick = () => {
42
- this.ui.setStatus(STATUS_KEY, `${SPINNER[this.frame % SPINNER.length]} ${label()}`);
43
- this.frame++;
44
- };
45
- tick();
46
- this.timer = setInterval(tick, 120);
47
- }
48
- /** Show a final, non-animated line, then clear it after a short beat. */
49
- finish(text) {
50
- this.stop();
51
- this.ui.setStatus(STATUS_KEY, text);
52
- if (text !== undefined) {
53
- setTimeout(() => this.ui.setStatus(STATUS_KEY, undefined), 4000);
54
- }
55
- }
56
- stop() {
57
- if (this.timer) {
58
- clearInterval(this.timer);
59
- this.timer = null;
60
- }
61
- }
62
- }
63
- async function resolveAuth(ctx, model) {
64
- try {
65
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- ctx.model is Model<any>; the registry wants Model<Api>
66
- const r = await ctx.modelRegistry.getApiKeyAndHeaders(model);
67
- return r.ok ? { apiKey: r.apiKey, headers: r.headers } : {};
68
- }
69
- catch {
70
- return {};
71
- }
72
- }
73
- const pct = (from, to) => Math.round((100 * (from - to)) / from);
74
- export function registerThinkingCompression(pi) {
75
- pi.on('message_end', async (event, ctx) => {
76
- if (!getConfig().compressReasoning)
77
- return;
78
- const message = event.message;
79
- const targets = collectCompressible(message, MIN_THINKING_CHARS);
80
- if (targets.length === 0)
81
- return;
82
- const model = ctx.model;
83
- if (!model)
84
- return;
85
- const loader = new Loader(ctx.ui);
86
- const auth = await resolveAuth(ctx, model);
87
- const modelRef = { id: model.id, baseUrl: model.baseUrl };
88
- const replacements = new Map();
89
- let origTotal = 0;
90
- let newTotal = 0;
91
- for (let i = 0; i < targets.length; i++) {
92
- const t = targets[i];
93
- const n = i + 1;
94
- loader.start(() => targets.length > 1 ?
95
- `compressing reasoning ${n}/${targets.length} (${t.text.length}c)…`
96
- : `compressing reasoning (${t.text.length}c)…`);
97
- try {
98
- const compressed = await compressOne(t.text, modelRef, auth);
99
- if (compressed.length > 0 && compressed.length < t.text.length) {
100
- replacements.set(t.index, compressed);
101
- origTotal += t.text.length;
102
- newTotal += compressed.length;
103
- }
104
- }
105
- catch {
106
- // Leave this block verbatim; move on to the next.
107
- }
108
- }
109
- if (replacements.size === 0) {
110
- loader.finish(undefined);
111
- return;
112
- }
113
- loader.finish(`✓ reasoning ${origTotal}→${newTotal}c (−${pct(origTotal, newTotal)}%)`);
114
- // Cast back to the concrete AgentMessage type: the helpers work on a
115
- // structural view, but the rewrite only swaps thinking-block text.
116
- return { message: rebuildWithCompressed(message, replacements) };
117
- });
118
- }
@@ -1,29 +0,0 @@
1
- /** Minimal structural view of a thinking content block. Kept structural (rather
2
- * than importing pi-ai's `ThinkingContent`) so these helpers stay pure and are
3
- * trivially unit-testable with plain objects. */
4
- export interface ThinkingBlock {
5
- type: 'thinking';
6
- thinking: string;
7
- thinkingSignature?: string;
8
- redacted?: boolean;
9
- }
10
- export interface AssistantMessageLike {
11
- role?: string;
12
- content?: unknown;
13
- }
14
- export interface CompressTarget {
15
- index: number;
16
- text: string;
17
- }
18
- /** Thinking blocks shorter than this aren't worth a model round-trip. */
19
- export declare const MIN_THINKING_CHARS = 120;
20
- export declare function isThinkingBlock(b: unknown): b is ThinkingBlock;
21
- export declare function isCompressible(b: ThinkingBlock, minChars: number): boolean;
22
- /** Compressible thinking blocks of an assistant message, with their positions. */
23
- export declare function collectCompressible(message: AssistantMessageLike, minChars: number): CompressTarget[];
24
- /** Rebuild an assistant message with compressed text swapped into the given
25
- * block indices. The block `type` and `thinkingSignature` are preserved so the
26
- * local provider still replays the (now shorter) reasoning, and a replacement is
27
- * only applied when it actually shrinks the block. Returns the same object when
28
- * nothing changed. */
29
- export declare function rebuildWithCompressed<T extends AssistantMessageLike>(message: T, byIndex: ReadonlyMap<number, string>): T;
@@ -1,53 +0,0 @@
1
- /** Thinking blocks shorter than this aren't worth a model round-trip. */
2
- export const MIN_THINKING_CHARS = 120;
3
- /** In `openai-completions` (llama.cpp/local) the "signature" is a field *name*
4
- * (`reasoning_content`) the reasoning is replayed under — not a crypto
5
- * signature — so rewriting the text is safe. A long, non-sentinel signature is
6
- * Anthropic-style extended thinking, where the signature cryptographically
7
- * signs the original text and the block feeds the next turn's continuation;
8
- * rewriting it would break that, so those blocks are skipped. */
9
- const SENTINEL_SIGNATURES = new Set(['', 'reasoning_content', 'reasoning', 'reasoning_text']);
10
- export function isThinkingBlock(b) {
11
- return (typeof b === 'object'
12
- && b !== null
13
- && b.type === 'thinking'
14
- && typeof b.thinking === 'string');
15
- }
16
- export function isCompressible(b, minChars) {
17
- if (b.redacted)
18
- return false;
19
- if (!SENTINEL_SIGNATURES.has(b.thinkingSignature ?? ''))
20
- return false;
21
- return b.thinking.trim().length >= minChars;
22
- }
23
- /** Compressible thinking blocks of an assistant message, with their positions. */
24
- export function collectCompressible(message, minChars) {
25
- if (message.role !== 'assistant' || !Array.isArray(message.content))
26
- return [];
27
- const out = [];
28
- message.content.forEach((b, index) => {
29
- if (isThinkingBlock(b) && isCompressible(b, minChars))
30
- out.push({ index, text: b.thinking });
31
- });
32
- return out;
33
- }
34
- /** Rebuild an assistant message with compressed text swapped into the given
35
- * block indices. The block `type` and `thinkingSignature` are preserved so the
36
- * local provider still replays the (now shorter) reasoning, and a replacement is
37
- * only applied when it actually shrinks the block. Returns the same object when
38
- * nothing changed. */
39
- export function rebuildWithCompressed(message, byIndex) {
40
- if (byIndex.size === 0 || !Array.isArray(message.content))
41
- return message;
42
- let changed = false;
43
- const content = message.content.map((b, index) => {
44
- const compressed = byIndex.get(index);
45
- if (compressed === undefined || !isThinkingBlock(b))
46
- return b;
47
- if (compressed.length >= b.thinking.length)
48
- return b;
49
- changed = true;
50
- return { ...b, thinking: compressed };
51
- });
52
- return changed ? { ...message, content } : message;
53
- }