@akagilnc/pi-workflow-roles 0.1.5204 → 0.1.5212

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.
@@ -1,98 +0,0 @@
1
- /**
2
- * Stream idle backstop (#102 / #155 a): first-wait + between-event silence only.
3
- * Not a total wall-clock for long thinking turns.
4
- *
5
- * Default silence budget is owner-final 183s (#102 2026-08-04),
6
- * same numeric value as header timeoutMs but a distinct body-idle clock.
7
- */
8
-
9
- export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 183_000;
10
- export const STREAM_IDLE_TIMEOUT_CODE = "AK_STREAM_IDLE_TIMEOUT" as const;
11
-
12
- export class StreamIdleTimeoutError extends Error {
13
- readonly code = STREAM_IDLE_TIMEOUT_CODE;
14
- readonly idleTimeoutMs: number;
15
-
16
- constructor(idleTimeoutMs: number, options?: ErrorOptions) {
17
- super(`stream idle timeout after ${idleTimeoutMs}ms`, options);
18
- this.name = "StreamIdleTimeoutError";
19
- this.idleTimeoutMs = idleTimeoutMs;
20
- }
21
- }
22
-
23
- export function isStreamIdleTimeoutError(value: unknown): value is StreamIdleTimeoutError {
24
- return value instanceof StreamIdleTimeoutError
25
- || (
26
- typeof value === "object"
27
- && value !== null
28
- && (value as { name?: unknown }).name === "StreamIdleTimeoutError"
29
- && (value as { code?: unknown }).code === STREAM_IDLE_TIMEOUT_CODE
30
- );
31
- }
32
-
33
- export type StreamIdleGuardOptions = {
34
- idleTimeoutMs?: number;
35
- parentSignal?: AbortSignal;
36
- };
37
-
38
- export type StreamIdleGuard = {
39
- readonly signal: AbortSignal;
40
- poke(): void;
41
- dispose(): void;
42
- };
43
-
44
- export function createStreamIdleGuard(
45
- options: StreamIdleGuardOptions = {},
46
- ): StreamIdleGuard {
47
- const idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
48
- const parentSignal = options.parentSignal;
49
-
50
- const controller = new AbortController();
51
- let timer: ReturnType<typeof setTimeout> | undefined;
52
- let disposed = false;
53
-
54
- const clear = (): void => {
55
- if (timer !== undefined) {
56
- clearTimeout(timer);
57
- timer = undefined;
58
- }
59
- };
60
-
61
- const arm = (): void => {
62
- if (disposed || controller.signal.aborted || idleTimeoutMs <= 0) return;
63
- clear();
64
- timer = setTimeout(() => {
65
- timer = undefined;
66
- if (disposed || controller.signal.aborted) return;
67
- controller.abort(new StreamIdleTimeoutError(idleTimeoutMs));
68
- }, idleTimeoutMs);
69
- };
70
-
71
- const onParentAbort = (): void => {
72
- if (controller.signal.aborted) return;
73
- clear();
74
- controller.abort(parentSignal?.reason);
75
- };
76
-
77
- if (parentSignal !== undefined) {
78
- if (parentSignal.aborted) {
79
- controller.abort(parentSignal.reason);
80
- } else {
81
- parentSignal.addEventListener("abort", onParentAbort);
82
- }
83
- }
84
-
85
- arm();
86
-
87
- return {
88
- signal: controller.signal,
89
- poke(): void {
90
- arm();
91
- },
92
- dispose(): void {
93
- disposed = true;
94
- clear();
95
- parentSignal?.removeEventListener("abort", onParentAbort);
96
- },
97
- };
98
- }
@@ -1,168 +0,0 @@
1
- import { writeSync } from "node:fs";
2
- import { Type, type Static } from "typebox";
3
- import { Value } from "typebox/value";
4
-
5
- import { writeStderrJsonlRecord } from "./stderr-jsonl.ts";
6
-
7
- /** Mandatory coalesce window for tool_execution_update heartbeats (stderr is bounded). */
8
- export const TOOL_EXECUTION_UPDATE_THROTTLE_MS = 30_000;
9
-
10
- /**
11
- * Heartbeat basis for downstream silence predicates.
12
- *
13
- * Production Pi `bash.js` calls `onUpdate` once with empty content at execute entry,
14
- * then schedules output-driven updates at ~100 ms. This face skips non-producing
15
- * (empty-content) updates so the first heartbeat is real child output, then
16
- * coalesces at {@link TOOL_EXECUTION_UPDATE_THROTTLE_MS}.
17
- *
18
- * If a future host path stopped delivering output-driven updates, change this to
19
- * `"duration-only"` and document that silence predicates degrade to pure duration.
20
- */
21
- export const TOOL_EXECUTION_UPDATE_HEARTBEAT = "output-driven" as const;
22
-
23
- const observationBase = {
24
- role: Type.String({ minLength: 1 }),
25
- toolCallId: Type.String({ minLength: 1 }),
26
- toolName: Type.String({ minLength: 1 }),
27
- timestamp: Type.String({ format: "date-time" }),
28
- } as const;
29
-
30
- export const toolExecutionObservationRecordSchema = Type.Union([
31
- Type.Object({
32
- ...observationBase,
33
- event: Type.Literal("tool_execution_start"),
34
- }, { additionalProperties: true }),
35
- Type.Object({
36
- ...observationBase,
37
- event: Type.Literal("tool_execution_update"),
38
- }, { additionalProperties: true }),
39
- Type.Object({
40
- ...observationBase,
41
- event: Type.Literal("tool_execution_end"),
42
- isError: Type.Boolean(),
43
- }, { additionalProperties: true }),
44
- ]);
45
-
46
- export type ToolExecutionObservationRecord = Static<typeof toolExecutionObservationRecordSchema>;
47
- export type ToolExecutionObservationWriter = (record: ToolExecutionObservationRecord) => void | Promise<void>;
48
-
49
- export function validateToolExecutionObservationRecord(record: unknown): ToolExecutionObservationRecord {
50
- if (!Value.Check(toolExecutionObservationRecordSchema, record)) {
51
- throw new TypeError("Tool execution observation record does not match its contract");
52
- }
53
- return record as ToolExecutionObservationRecord;
54
- }
55
-
56
- export function writeToolExecutionObservationRecord(
57
- record: ToolExecutionObservationRecord,
58
- write: typeof writeSync = writeSync,
59
- ): void {
60
- writeStderrJsonlRecord(validateToolExecutionObservationRecord(record), write);
61
- }
62
-
63
- /** True when a tool_execution_update partialResult carries producing content (not bash's empty entry callback). */
64
- export function isProducingToolUpdate(partialResult: unknown): boolean {
65
- if (partialResult == null) return false;
66
- if (typeof partialResult !== "object") return true;
67
- const content = (partialResult as { content?: unknown }).content;
68
- if (!Array.isArray(content)) return true;
69
- if (content.length === 0) return false;
70
- return content.some((part) => {
71
- if (typeof part !== "object" || part === null) return true;
72
- const text = (part as { text?: unknown }).text;
73
- if (typeof text === "string") return text.length > 0;
74
- return true;
75
- });
76
- }
77
-
78
- type CallObservationState = {
79
- lastUpdateEmitMonoMs: number | undefined;
80
- };
81
-
82
- export type ToolExecutionObservationFace = {
83
- onStart(event: { toolCallId: string; toolName: string }): void | Promise<void>;
84
- onUpdate(event: { toolCallId: string; toolName: string; partialResult: unknown }): void | Promise<void>;
85
- onEnd(event: { toolCallId: string; toolName: string; isError: boolean }): void | Promise<void>;
86
- reset(): void;
87
- };
88
-
89
- /**
90
- * Production throttle clock: genuinely monotonic milliseconds.
91
- * Wall-clock Date.now() is not safe — NTP/slew can compress two heartbeats under 30s.
92
- */
93
- export function systemToolExecutionObservationMonoNow(): number {
94
- return performance.now();
95
- }
96
-
97
- export function createToolExecutionObservationFace(options: {
98
- role: () => string | undefined;
99
- admitted: () => boolean;
100
- clock(): string;
101
- monoNow(): number;
102
- write(record: ToolExecutionObservationRecord): void | Promise<void>;
103
- }): ToolExecutionObservationFace {
104
- const states = new Map<string, CallObservationState>();
105
-
106
- async function emit(record: unknown): Promise<void> {
107
- await options.write(validateToolExecutionObservationRecord(record));
108
- }
109
-
110
- function activeRole(): string | undefined {
111
- if (!options.admitted()) return undefined;
112
- const role = options.role();
113
- return role === undefined || role === "" ? undefined : role;
114
- }
115
-
116
- return {
117
- reset() {
118
- states.clear();
119
- },
120
- async onStart(event) {
121
- const role = activeRole();
122
- if (role === undefined) return;
123
- states.set(event.toolCallId, { lastUpdateEmitMonoMs: undefined });
124
- await emit({
125
- event: "tool_execution_start",
126
- role,
127
- toolCallId: event.toolCallId,
128
- toolName: event.toolName,
129
- timestamp: options.clock(),
130
- });
131
- },
132
- async onUpdate(event) {
133
- const role = activeRole();
134
- if (role === undefined) return;
135
- if (!isProducingToolUpdate(event.partialResult)) return;
136
- const now = options.monoNow();
137
- const state = states.get(event.toolCallId) ?? { lastUpdateEmitMonoMs: undefined };
138
- if (
139
- state.lastUpdateEmitMonoMs !== undefined
140
- && now - state.lastUpdateEmitMonoMs < TOOL_EXECUTION_UPDATE_THROTTLE_MS
141
- ) {
142
- return;
143
- }
144
- state.lastUpdateEmitMonoMs = now;
145
- states.set(event.toolCallId, state);
146
- await emit({
147
- event: "tool_execution_update",
148
- role,
149
- toolCallId: event.toolCallId,
150
- toolName: event.toolName,
151
- timestamp: options.clock(),
152
- });
153
- },
154
- async onEnd(event) {
155
- const role = activeRole();
156
- if (role === undefined) return;
157
- states.delete(event.toolCallId);
158
- await emit({
159
- event: "tool_execution_end",
160
- role,
161
- toolCallId: event.toolCallId,
162
- toolName: event.toolName,
163
- timestamp: options.clock(),
164
- isError: event.isError,
165
- });
166
- },
167
- };
168
- }