takomi 2.1.28 → 2.1.30
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/.pi/agents/coder.md +6 -0
- package/.pi/agents/orchestrator.md +16 -0
- package/.pi/extensions/oauth-router/README.md +4 -2
- package/.pi/extensions/oauth-router/commands.ts +38 -14
- package/.pi/extensions/oauth-router/config.ts +12 -3
- package/.pi/extensions/oauth-router/index.ts +1 -1
- package/.pi/extensions/oauth-router/oauth-flow.ts +105 -25
- package/.pi/extensions/oauth-router/provider.ts +112 -12
- package/.pi/extensions/oauth-router/state.ts +12 -1
- package/.pi/extensions/oauth-router/types.ts +17 -2
- package/.pi/extensions/takomi-runtime/context-panel.ts +153 -28
- package/.pi/extensions/takomi-runtime/index.ts +2 -0
- package/.pi/extensions/takomi-runtime/takomi-stats.js +679 -679
- package/.pi/prompts/build-prompt.md +15 -0
- package/.pi/prompts/genesis-prompt.md +8 -0
- package/.pi/prompts/takomi-prompt.md +16 -0
- package/package.json +7 -5
- package/src/pi-harness.js +53 -6
- package/src/takomi-stats.js +749 -679
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Context, Model, SimpleStreamOptions } from "@
|
|
1
|
+
import type { Api, AssistantMessage, Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
3
|
export type RouterUpstreamApi = "openai-completions" | "openai-responses" | "openai-codex-responses";
|
|
4
4
|
export type RouterAuthMode = "oauth" | "api-key";
|
|
@@ -27,6 +27,8 @@ export interface RouterModelConfig {
|
|
|
27
27
|
export interface RouterProviderUsageProbeConfig {
|
|
28
28
|
enabled?: boolean;
|
|
29
29
|
endpoints?: string[];
|
|
30
|
+
/** Total best-effort probe budget per account. Provider usage checks must not block login/refresh UX. */
|
|
31
|
+
timeoutMs?: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface RouterUpstreamConfig {
|
|
@@ -50,6 +52,18 @@ export interface RouterConfig {
|
|
|
50
52
|
tokenRefreshSkewMs: number;
|
|
51
53
|
rateLimitCooldownMs: number;
|
|
52
54
|
transientPenaltyMs: number;
|
|
55
|
+
/** Default retry attempts passed to upstream providers when the caller does not specify one. */
|
|
56
|
+
upstreamMaxRetries: number;
|
|
57
|
+
/** Maximum provider-request retry delay when supported by the upstream provider. */
|
|
58
|
+
upstreamMaxRetryDelayMs: number;
|
|
59
|
+
/** Router-level same-account retries for local/client transport failures before account failover. */
|
|
60
|
+
clientNetworkMaxRetries: number;
|
|
61
|
+
/** First router-level retry delay for local/client transport failures. Later retries double this delay. */
|
|
62
|
+
clientNetworkRetryBaseDelayMs: number;
|
|
63
|
+
/** Maximum router-level retry delay for local/client transport failures. */
|
|
64
|
+
clientNetworkMaxRetryDelayMs: number;
|
|
65
|
+
/** Penalty for local/client transport failures. Zero records the failure without cooling down the account. */
|
|
66
|
+
clientNetworkPenaltyMs: number;
|
|
53
67
|
models: RouterModelConfig[];
|
|
54
68
|
upstreams: RouterUpstreamConfig[];
|
|
55
69
|
}
|
|
@@ -178,6 +192,7 @@ export interface RouterStatusRow {
|
|
|
178
192
|
rateLimitCount: number;
|
|
179
193
|
authFailureCount: number;
|
|
180
194
|
successCount: number;
|
|
195
|
+
lastError?: string;
|
|
181
196
|
expires: number;
|
|
182
197
|
}
|
|
183
198
|
|
|
@@ -198,7 +213,7 @@ export interface DelegateSelection {
|
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
export interface FailureClassification {
|
|
201
|
-
kind: "rate-limit" | "auth" | "transient" | "fatal";
|
|
216
|
+
kind: "rate-limit" | "auth" | "transient" | "client-network" | "fatal";
|
|
202
217
|
status?: number;
|
|
203
218
|
retryAfterMs?: number;
|
|
204
219
|
message: string;
|
|
@@ -53,6 +53,8 @@ export type ContextPanelState = {
|
|
|
53
53
|
toolUses: ToolUseCount;
|
|
54
54
|
sessionStart: number;
|
|
55
55
|
activeMs: number;
|
|
56
|
+
activityIntervals: ActivityInterval[];
|
|
57
|
+
pendingToolStarts: Record<string, number>;
|
|
56
58
|
lastActivityAt: number;
|
|
57
59
|
lastToolAt: number;
|
|
58
60
|
runtime: ContextRuntimeState;
|
|
@@ -70,12 +72,28 @@ type BoardCounts = {
|
|
|
70
72
|
blocked: number;
|
|
71
73
|
};
|
|
72
74
|
|
|
75
|
+
type BoardCountsCacheEntry = {
|
|
76
|
+
checkedAt: number;
|
|
77
|
+
mtimeMs: number;
|
|
78
|
+
counts?: BoardCounts;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type ActivityInterval = {
|
|
82
|
+
start: number;
|
|
83
|
+
end: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const BOARD_COUNTS_CACHE_MS = 1_000;
|
|
87
|
+
const boardCountsCache = new Map<string, BoardCountsCacheEntry>();
|
|
88
|
+
|
|
73
89
|
function createEmptyState(sessionStart = Date.now(), runtime: ContextRuntimeState = {}): ContextPanelState {
|
|
74
90
|
return {
|
|
75
91
|
fileChanges: [],
|
|
76
92
|
toolUses: {},
|
|
77
93
|
sessionStart,
|
|
78
94
|
activeMs: 0,
|
|
95
|
+
activityIntervals: [],
|
|
96
|
+
pendingToolStarts: {},
|
|
79
97
|
lastActivityAt: sessionStart,
|
|
80
98
|
lastToolAt: 0,
|
|
81
99
|
runtime,
|
|
@@ -164,14 +182,28 @@ function toTimestampMs(entry: { timestamp?: unknown; message?: { timestamp?: unk
|
|
|
164
182
|
return undefined;
|
|
165
183
|
}
|
|
166
184
|
|
|
167
|
-
function
|
|
168
|
-
const sorted =
|
|
169
|
-
|
|
185
|
+
function mergeIntervals(intervals: ActivityInterval[]): ActivityInterval[] {
|
|
186
|
+
const sorted = intervals
|
|
187
|
+
.filter((interval) => Number.isFinite(interval.start) && Number.isFinite(interval.end) && interval.end > interval.start)
|
|
188
|
+
.map((interval) => ({ start: interval.start, end: interval.end }))
|
|
189
|
+
.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
190
|
+
if (sorted.length === 0) return [];
|
|
191
|
+
|
|
192
|
+
const merged: ActivityInterval[] = [sorted[0]];
|
|
170
193
|
for (let i = 1; i < sorted.length; i += 1) {
|
|
171
|
-
const
|
|
172
|
-
|
|
194
|
+
const current = sorted[i];
|
|
195
|
+
const previous = merged[merged.length - 1];
|
|
196
|
+
if (current.start <= previous.end) {
|
|
197
|
+
previous.end = Math.max(previous.end, current.end);
|
|
198
|
+
} else {
|
|
199
|
+
merged.push({ ...current });
|
|
200
|
+
}
|
|
173
201
|
}
|
|
174
|
-
return
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function calculateActiveMs(intervals: ActivityInterval[]): number {
|
|
206
|
+
return mergeIntervals(intervals).reduce((total, interval) => total + (interval.end - interval.start), 0);
|
|
175
207
|
}
|
|
176
208
|
|
|
177
209
|
function formatDisplayPath(filePath: string, cwd?: string): string {
|
|
@@ -186,8 +218,19 @@ function formatDisplayPath(filePath: string, cwd?: string): string {
|
|
|
186
218
|
|
|
187
219
|
function loadBoardCounts(cwd: string, sessionId?: string): BoardCounts | undefined {
|
|
188
220
|
if (!sessionId) return undefined;
|
|
221
|
+
const stateFile = path.join(cwd, ".pi", "takomi", "orchestrator", `${sessionId}.json`);
|
|
222
|
+
const cached = boardCountsCache.get(stateFile);
|
|
223
|
+
const checkedAt = Date.now();
|
|
224
|
+
|
|
225
|
+
if (cached && checkedAt - cached.checkedAt < BOARD_COUNTS_CACHE_MS) return cached.counts;
|
|
226
|
+
|
|
189
227
|
try {
|
|
190
|
-
const
|
|
228
|
+
const mtimeMs = statSync(stateFile).mtimeMs;
|
|
229
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
230
|
+
boardCountsCache.set(stateFile, { ...cached, checkedAt });
|
|
231
|
+
return cached.counts;
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
const parsed = JSON.parse(readFileSync(stateFile, "utf8")) as { tasks?: Array<{ status?: string }> };
|
|
192
235
|
const counts: BoardCounts = { completed: 0, pending: 0, inProgress: 0, blocked: 0 };
|
|
193
236
|
for (const task of parsed.tasks ?? []) {
|
|
@@ -196,8 +239,10 @@ function loadBoardCounts(cwd: string, sessionId?: string): BoardCounts | undefin
|
|
|
196
239
|
else if (task.status === "blocked") counts.blocked += 1;
|
|
197
240
|
else counts.pending += 1;
|
|
198
241
|
}
|
|
242
|
+
boardCountsCache.set(stateFile, { checkedAt, mtimeMs, counts });
|
|
199
243
|
return counts;
|
|
200
244
|
} catch {
|
|
245
|
+
boardCountsCache.set(stateFile, { checkedAt, mtimeMs: 0, counts: undefined });
|
|
201
246
|
return undefined;
|
|
202
247
|
}
|
|
203
248
|
}
|
|
@@ -268,7 +313,10 @@ class ContextPanelComponent implements Component {
|
|
|
268
313
|
|
|
269
314
|
if (ctx) {
|
|
270
315
|
const age = formatDuration(Date.now() - state.sessionStart);
|
|
271
|
-
const
|
|
316
|
+
const pendingStarts = Object.values(state.pendingToolStarts ?? {});
|
|
317
|
+
const pendingStart = pendingStarts.length > 0 ? Math.min(...pendingStarts) : undefined;
|
|
318
|
+
const activeMs = pendingStart !== undefined ? state.activeMs + Math.max(0, Date.now() - pendingStart) : state.activeMs;
|
|
319
|
+
const active = formatDuration(activeMs);
|
|
272
320
|
lines.push(`${pad}${theme.fg("dim", "Age:")} ${theme.fg("muted", age)}`);
|
|
273
321
|
lines.push(`${pad}${theme.fg("dim", "Active:")} ${theme.fg("muted", active)}`);
|
|
274
322
|
|
|
@@ -354,6 +402,7 @@ export class TakomiContextPanel {
|
|
|
354
402
|
private overlayHandle?: OverlayHandle;
|
|
355
403
|
private requestRender?: () => void;
|
|
356
404
|
private lastCtx?: ExtensionContext;
|
|
405
|
+
private readonly toolStartTimes = new Map<string, number>();
|
|
357
406
|
|
|
358
407
|
getState(): ContextPanelState {
|
|
359
408
|
return this.state;
|
|
@@ -368,6 +417,33 @@ export class TakomiContextPanel {
|
|
|
368
417
|
this.requestRender?.();
|
|
369
418
|
}
|
|
370
419
|
|
|
420
|
+
private recomputeActiveMs(): void {
|
|
421
|
+
this.state.activeMs = calculateActiveMs(this.state.activityIntervals);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private addActivityInterval(start: number, end: number): void {
|
|
425
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return;
|
|
426
|
+
this.state.activityIntervals.push({ start, end });
|
|
427
|
+
this.recomputeActiveMs();
|
|
428
|
+
this.state.lastActivityAt = Math.max(this.state.lastActivityAt, end);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private noteActivity(timestamp = Date.now(), allowLongGap = false): void {
|
|
432
|
+
const previous = this.state.lastActivityAt;
|
|
433
|
+
const delta = timestamp - previous;
|
|
434
|
+
if (delta > 0 && (allowLongGap || delta <= ACTIVE_GAP_THRESHOLD_MS)) {
|
|
435
|
+
this.addActivityInterval(previous, timestamp);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.state.lastActivityAt = Math.max(previous, timestamp);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private noteToolWindow(toolCallId: string | undefined, start: number, end: number): void {
|
|
442
|
+
this.noteActivity(start);
|
|
443
|
+
this.addActivityInterval(start, end);
|
|
444
|
+
if (toolCallId) delete this.state.pendingToolStarts[toolCallId];
|
|
445
|
+
}
|
|
446
|
+
|
|
371
447
|
trackFileChange(change: FileChange): void {
|
|
372
448
|
this.state.fileChanges.push({ ...change, timestamp: change.timestamp || Date.now() });
|
|
373
449
|
this.state.lastToolAt = Date.now();
|
|
@@ -375,21 +451,39 @@ export class TakomiContextPanel {
|
|
|
375
451
|
}
|
|
376
452
|
|
|
377
453
|
trackActivity(timestamp = Date.now()): void {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
454
|
+
this.noteActivity(timestamp);
|
|
455
|
+
this.requestRender?.();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
trackToolStart(toolCallId: string | undefined, timestamp = Date.now()): void {
|
|
459
|
+
if (toolCallId) {
|
|
460
|
+
this.toolStartTimes.set(toolCallId, timestamp);
|
|
461
|
+
this.state.pendingToolStarts[toolCallId] = timestamp;
|
|
462
|
+
}
|
|
463
|
+
this.noteActivity(timestamp);
|
|
464
|
+
this.requestRender?.();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
trackToolEnd(toolCallId: string | undefined, timestamp = Date.now()): void {
|
|
468
|
+
const start = toolCallId ? this.toolStartTimes.get(toolCallId) : undefined;
|
|
469
|
+
if (start !== undefined) {
|
|
470
|
+
this.noteToolWindow(toolCallId, start, timestamp);
|
|
471
|
+
if (toolCallId) this.toolStartTimes.delete(toolCallId);
|
|
472
|
+
} else {
|
|
473
|
+
if (toolCallId) delete this.state.pendingToolStarts[toolCallId];
|
|
474
|
+
this.noteActivity(timestamp);
|
|
475
|
+
}
|
|
382
476
|
this.requestRender?.();
|
|
383
477
|
}
|
|
384
478
|
|
|
385
479
|
trackToolUse(toolName: string): void {
|
|
386
480
|
this.state.toolUses[toolName] = (this.state.toolUses[toolName] ?? 0) + 1;
|
|
387
481
|
this.state.lastToolAt = Date.now();
|
|
388
|
-
this.trackActivity(this.state.lastToolAt);
|
|
389
482
|
}
|
|
390
483
|
|
|
391
484
|
resetSession(): void {
|
|
392
485
|
this.state = createEmptyState(Date.now(), this.state.runtime);
|
|
486
|
+
this.toolStartTimes.clear();
|
|
393
487
|
this.requestRender?.();
|
|
394
488
|
}
|
|
395
489
|
|
|
@@ -416,36 +510,65 @@ export class TakomiContextPanel {
|
|
|
416
510
|
timestamp?: number;
|
|
417
511
|
};
|
|
418
512
|
}>;
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
513
|
+
const next = createEmptyState(Date.now(), this.state.runtime);
|
|
514
|
+
const intervals: ActivityInterval[] = [];
|
|
515
|
+
const toolCalls = new Map<string, { name: string; input: ToolCallInput; startedAt: number }>();
|
|
516
|
+
let lastActivityPoint: number | undefined;
|
|
517
|
+
let firstSessionTs: number | undefined;
|
|
518
|
+
this.toolStartTimes.clear();
|
|
425
519
|
|
|
426
520
|
for (const entry of branch) {
|
|
521
|
+
const ts = toTimestampMs(entry);
|
|
427
522
|
const message = entry.message;
|
|
428
|
-
if (
|
|
523
|
+
if (ts !== undefined) firstSessionTs = firstSessionTs === undefined ? ts : Math.min(firstSessionTs, ts);
|
|
524
|
+
if (!message || ts === undefined) continue;
|
|
525
|
+
|
|
429
526
|
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
527
|
+
let sawToolCall = false;
|
|
430
528
|
for (const block of message.content) {
|
|
431
529
|
const record = asRecord(block);
|
|
432
530
|
if (record.type !== "toolCall") continue;
|
|
433
531
|
const id = typeof record.id === "string" ? record.id : undefined;
|
|
434
532
|
const name = typeof record.name === "string" ? record.name : undefined;
|
|
435
533
|
if (!id || !name) continue;
|
|
436
|
-
|
|
534
|
+
sawToolCall = true;
|
|
535
|
+
toolCalls.set(id, { name, input: asRecord(record.arguments), startedAt: ts });
|
|
536
|
+
this.toolStartTimes.set(id, ts);
|
|
537
|
+
}
|
|
538
|
+
if (lastActivityPoint !== undefined && ts > lastActivityPoint && ts - lastActivityPoint <= ACTIVE_GAP_THRESHOLD_MS) {
|
|
539
|
+
intervals.push({ start: lastActivityPoint, end: ts });
|
|
437
540
|
}
|
|
541
|
+
lastActivityPoint = ts;
|
|
542
|
+
if (!sawToolCall) continue;
|
|
438
543
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
544
|
+
|
|
545
|
+
if (message.role === "toolResult" && message.toolName) {
|
|
546
|
+
next.toolUses[message.toolName] = (next.toolUses[message.toolName] ?? 0) + 1;
|
|
547
|
+
next.lastToolAt = ts;
|
|
548
|
+
const toolCall = message.toolCallId ? toolCalls.get(message.toolCallId) : undefined;
|
|
549
|
+
if (toolCall) {
|
|
550
|
+
intervals.push({ start: toolCall.startedAt, end: ts });
|
|
551
|
+
const change = changeFromTool(message.toolName, toolCall.input);
|
|
552
|
+
if (change) next.fileChanges.push({ ...change, timestamp: ts });
|
|
553
|
+
if (message.toolCallId) toolCalls.delete(message.toolCallId);
|
|
554
|
+
} else if (lastActivityPoint !== undefined && ts > lastActivityPoint && ts - lastActivityPoint <= ACTIVE_GAP_THRESHOLD_MS) {
|
|
555
|
+
intervals.push({ start: lastActivityPoint, end: ts });
|
|
556
|
+
}
|
|
557
|
+
lastActivityPoint = ts;
|
|
558
|
+
continue;
|
|
446
559
|
}
|
|
560
|
+
|
|
561
|
+
if (lastActivityPoint !== undefined && ts > lastActivityPoint && ts - lastActivityPoint <= ACTIVE_GAP_THRESHOLD_MS) {
|
|
562
|
+
intervals.push({ start: lastActivityPoint, end: ts });
|
|
563
|
+
}
|
|
564
|
+
lastActivityPoint = ts;
|
|
447
565
|
}
|
|
448
566
|
|
|
567
|
+
next.sessionStart = firstSessionTs ?? next.sessionStart;
|
|
568
|
+
next.activityIntervals = intervals;
|
|
569
|
+
next.activeMs = calculateActiveMs(intervals);
|
|
570
|
+
next.lastActivityAt = lastActivityPoint ?? next.sessionStart;
|
|
571
|
+
|
|
449
572
|
this.state = next;
|
|
450
573
|
this.requestRender?.();
|
|
451
574
|
}
|
|
@@ -507,6 +630,7 @@ export function wireContextPanel(pi: ExtensionAPI, panel: TakomiContextPanel): v
|
|
|
507
630
|
const pendingWriteActions = new Map<string, "M" | "+">();
|
|
508
631
|
|
|
509
632
|
pi.on("tool_call", (event, ctx) => {
|
|
633
|
+
panel.trackToolStart(event.toolCallId, Date.now());
|
|
510
634
|
if (event.toolName !== "write") return;
|
|
511
635
|
const input = asRecord(event.input);
|
|
512
636
|
const filePath = normalizeToolPath(input);
|
|
@@ -524,6 +648,7 @@ export function wireContextPanel(pi: ExtensionAPI, panel: TakomiContextPanel): v
|
|
|
524
648
|
pi.on("tool_result", (event) => {
|
|
525
649
|
const toolName = event.toolName;
|
|
526
650
|
panel.trackToolUse(toolName);
|
|
651
|
+
panel.trackToolEnd(event.toolCallId, Date.now());
|
|
527
652
|
|
|
528
653
|
const input = asRecord(event.input);
|
|
529
654
|
const actionOverride = toolName === "write" ? pendingWriteActions.get(event.toolCallId) : undefined;
|
|
@@ -891,6 +891,8 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
891
891
|
"Session IDs must use the canonical timestamp format orch-YYYYMMDD-HHMMSS. Use the same sessionId for the authored docs folder and the board JSON state.",
|
|
892
892
|
"For high-quality orchestration sessions, provide sessionId, masterPlanMarkdown, and taskMarkdown values that match the authored session folder. If you already wrote docs/tasks/orchestrator-sessions/<id>, call this tool with sessionId=<id>; do not create a second session id.",
|
|
893
893
|
"JSON fields should carry IDs/status/roles/workflow/dependencies/checklists for tracking, not replace expressive markdown.",
|
|
894
|
+
"Do not use expand_stage as a placeholder generator. For Design/Build expansions, provide full taskMarkdown or complete objective/scope/definitionOfDone/expectedArtifacts/instructions for every task.",
|
|
895
|
+
"If a task packet would render with Scope/Definition Of Done/Expected Artifacts as None specified, repair it before launching subagents.",
|
|
894
896
|
"A new session should normally begin Genesis-first, then expand Design and Build into as many tasks as the scope actually needs.",
|
|
895
897
|
"If the request is small enough, do not force orchestration just because the tool exists.",
|
|
896
898
|
"If a reviewed task needs more work, reuse the task conversationId when you call takomi_subagent again, then update the board with the new result.",
|