@code-yeongyu/senpi-agent-core 2026.10.21-1 → 2026.10.21-2
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/dist/agent-loop.js +11 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/response-timing.d.ts +31 -0
- package/dist/response-timing.js +66 -0
- package/package.json +3 -3
package/dist/agent-loop.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createInitialSystemMessage, EventStream, getCurrentTools, getToolStateChanges, isCursorExecResolved, normalizeContext, supportsAllowedToolChoice, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
6
6
|
import { createTerminalFailureAssistantMessage, demoteToolUseWithoutToolCalls, isStreamIdleTimeoutError, normalizeTerminalAssistantMessage, promoteStopWithPendingToolCalls, shouldFinalizeIdleAsStop, shouldTerminateAssistantTurn, } from "./assistant-terminal-state.js";
|
|
7
|
+
import { createResponseTimingRecorder } from "./response-timing.js";
|
|
7
8
|
import { getDefaultStreamFn, withEmptyAssistantRecovery } from "./stream-fn.js";
|
|
8
9
|
import { prepareAgentToolCallArguments } from "./tool-arguments.js";
|
|
9
10
|
import { resolveCallTool, withToolNameCorrection } from "./tool-name-alias.js";
|
|
@@ -445,7 +446,9 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
445
446
|
// after the assistant message so pairs stay adjacent in the transcript.
|
|
446
447
|
const providerToolResults = [];
|
|
447
448
|
const thinkingTiming = new Map();
|
|
448
|
-
|
|
449
|
+
const responseTiming = createResponseTimingRecorder();
|
|
450
|
+
function stampTiming(finalMessage) {
|
|
451
|
+
responseTiming.stamp(finalMessage);
|
|
449
452
|
for (const timing of thinkingTiming.values()) {
|
|
450
453
|
if (timing.endedAt === undefined)
|
|
451
454
|
timing.endedAt = Date.now();
|
|
@@ -480,6 +483,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
480
483
|
const llmContext = await preStream.run(buildProviderContext(context, config, requestAbortController.signal));
|
|
481
484
|
// Resolve API key (important for expiring tokens)
|
|
482
485
|
const resolvedApiKey = (config.getApiKey ? await preStream.run(config.getApiKey(config.model.provider)) : undefined) || config.apiKey;
|
|
486
|
+
responseTiming.requested();
|
|
483
487
|
const response = await preStream.run(streamFunction(config.model, llmContext, {
|
|
484
488
|
...config,
|
|
485
489
|
streamKind: "main",
|
|
@@ -516,6 +520,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
516
520
|
const event = next.value;
|
|
517
521
|
switch (event.type) {
|
|
518
522
|
case "start":
|
|
523
|
+
responseTiming.started();
|
|
519
524
|
partialMessage = event.partial;
|
|
520
525
|
context.messages.push(partialMessage);
|
|
521
526
|
addedPartial = true;
|
|
@@ -535,6 +540,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
535
540
|
case "toolcall_end":
|
|
536
541
|
if (partialMessage) {
|
|
537
542
|
partialMessage = event.partial;
|
|
543
|
+
responseTiming.observe(event);
|
|
538
544
|
if (event.type === "thinking_start" ||
|
|
539
545
|
event.type === "thinking_delta" ||
|
|
540
546
|
event.type === "thinking_end") {
|
|
@@ -563,7 +569,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
563
569
|
case "done":
|
|
564
570
|
case "error": {
|
|
565
571
|
const finalMessage = normalizeTerminalAssistantMessage(await result(), event);
|
|
566
|
-
|
|
572
|
+
stampTiming(finalMessage);
|
|
567
573
|
if (addedPartial) {
|
|
568
574
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
569
575
|
}
|
|
@@ -586,7 +592,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
586
592
|
eventReader.dispose();
|
|
587
593
|
}
|
|
588
594
|
const finalMessage = await result();
|
|
589
|
-
|
|
595
|
+
stampTiming(finalMessage);
|
|
590
596
|
if (addedPartial) {
|
|
591
597
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
592
598
|
}
|
|
@@ -619,7 +625,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
619
625
|
stopReason: "stop",
|
|
620
626
|
timestamp: partialMessage?.timestamp ?? Date.now(),
|
|
621
627
|
};
|
|
622
|
-
|
|
628
|
+
stampTiming(finalMessage);
|
|
623
629
|
if (addedPartial) {
|
|
624
630
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
625
631
|
}
|
|
@@ -637,7 +643,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
637
643
|
const callerAbortedPastBound = signal?.aborted === true &&
|
|
638
644
|
(error instanceof StreamStartTimeoutError || isStreamIdleTimeoutError(error) || error === signal.reason);
|
|
639
645
|
const finalMessage = createTerminalFailureAssistantMessage(config.model, signal?.aborted ? "aborted" : "error", callerAbortedPastBound ? abortError(signal.reason) : error, partialMessage);
|
|
640
|
-
|
|
646
|
+
stampTiming(finalMessage);
|
|
641
647
|
if (addedPartial) {
|
|
642
648
|
context.messages[context.messages.length - 1] = finalMessage;
|
|
643
649
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from "./harness/utils/segmented-read-view.ts";
|
|
|
26
26
|
export * from "./harness/utils/shell-output.ts";
|
|
27
27
|
export * from "./harness/utils/truncate.ts";
|
|
28
28
|
export * from "./proxy.ts";
|
|
29
|
+
export * from "./response-timing.ts";
|
|
29
30
|
export * from "./search/index.ts";
|
|
30
31
|
export { setDefaultStreamFn } from "./stream-fn.ts";
|
|
31
32
|
export { prepareToolArguments } from "./tool-arguments.ts";
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export * from "./harness/utils/segmented-read-view.js";
|
|
|
24
24
|
export * from "./harness/utils/shell-output.js";
|
|
25
25
|
export * from "./harness/utils/truncate.js";
|
|
26
26
|
export * from "./proxy.js";
|
|
27
|
+
export * from "./response-timing.js";
|
|
27
28
|
export * from "./search/index.js";
|
|
28
29
|
export { setDefaultStreamFn } from "./stream-fn.js";
|
|
29
30
|
export { prepareToolArguments } from "./tool-arguments.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
|
|
2
|
+
/** The deltas that count as streamed output for time to first token and the generation window. */
|
|
3
|
+
export declare const STREAMED_DELTA_TYPES: ReadonlySet<string>;
|
|
4
|
+
export type GenerationWindowMessage = {
|
|
5
|
+
readonly usage?: {
|
|
6
|
+
readonly reasoning?: number;
|
|
7
|
+
};
|
|
8
|
+
readonly content?: ReadonlyArray<{
|
|
9
|
+
readonly type?: string;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Whether a response generated output before its first streamed delta, so its generation window starts at the
|
|
14
|
+
* response start instead (senpi#3393). Reasoning counted in usage.output may be hidden or only summarised after it
|
|
15
|
+
* finished; a thinking block whose text never streamed (Bedrock, omitted display) reports no reasoning count. Shared
|
|
16
|
+
* by the persisted timing (senpi#3879) and the TPS notice so both measure the same window.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reasonedBeforeFirstDelta(message: GenerationWindowMessage, thinkingStreamed: boolean): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Records one response's timing as the agent loop streams it, then stamps it on the final message (senpi#3879).
|
|
21
|
+
* Wall-clock fields use `Date.now()` so they line up with `timestamp`; the generation window uses the monotonic
|
|
22
|
+
* clock, like the TPS notice, so a clock jump cannot make it negative.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createResponseTimingRecorder(): {
|
|
25
|
+
requested(): void;
|
|
26
|
+
started(): void;
|
|
27
|
+
observe(event: AssistantMessageEvent): void;
|
|
28
|
+
/** Stamps the final message. A response whose request was never issued gets no timing. */
|
|
29
|
+
stamp(message: AssistantMessage): void;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=response-timing.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** The deltas that count as streamed output for time to first token and the generation window. */
|
|
2
|
+
export const STREAMED_DELTA_TYPES = new Set(["text_delta", "thinking_delta", "toolcall_delta"]);
|
|
3
|
+
/**
|
|
4
|
+
* Whether a response generated output before its first streamed delta, so its generation window starts at the
|
|
5
|
+
* response start instead (senpi#3393). Reasoning counted in usage.output may be hidden or only summarised after it
|
|
6
|
+
* finished; a thinking block whose text never streamed (Bedrock, omitted display) reports no reasoning count. Shared
|
|
7
|
+
* by the persisted timing (senpi#3879) and the TPS notice so both measure the same window.
|
|
8
|
+
*/
|
|
9
|
+
export function reasonedBeforeFirstDelta(message, thinkingStreamed) {
|
|
10
|
+
if ((message.usage?.reasoning ?? 0) > 0)
|
|
11
|
+
return true;
|
|
12
|
+
if (thinkingStreamed)
|
|
13
|
+
return false;
|
|
14
|
+
return message.content?.some((block) => block.type === "thinking") ?? false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Records one response's timing as the agent loop streams it, then stamps it on the final message (senpi#3879).
|
|
18
|
+
* Wall-clock fields use `Date.now()` so they line up with `timestamp`; the generation window uses the monotonic
|
|
19
|
+
* clock, like the TPS notice, so a clock jump cannot make it negative.
|
|
20
|
+
*/
|
|
21
|
+
export function createResponseTimingRecorder() {
|
|
22
|
+
let requestedAt;
|
|
23
|
+
let startedMs;
|
|
24
|
+
let firstDeltaMs;
|
|
25
|
+
let lastDeltaMs;
|
|
26
|
+
let firstTokenAt;
|
|
27
|
+
let lastTokenAt;
|
|
28
|
+
let thinkingStreamed = false;
|
|
29
|
+
return {
|
|
30
|
+
requested() {
|
|
31
|
+
requestedAt = Date.now();
|
|
32
|
+
},
|
|
33
|
+
started() {
|
|
34
|
+
startedMs ??= performance.now();
|
|
35
|
+
},
|
|
36
|
+
observe(event) {
|
|
37
|
+
if (!STREAMED_DELTA_TYPES.has(event.type))
|
|
38
|
+
return;
|
|
39
|
+
const now = performance.now();
|
|
40
|
+
const at = Date.now();
|
|
41
|
+
if (event.type === "thinking_delta")
|
|
42
|
+
thinkingStreamed = true;
|
|
43
|
+
firstDeltaMs ??= now;
|
|
44
|
+
firstTokenAt ??= at;
|
|
45
|
+
lastDeltaMs = now;
|
|
46
|
+
lastTokenAt = at;
|
|
47
|
+
},
|
|
48
|
+
/** Stamps the final message. A response whose request was never issued gets no timing. */
|
|
49
|
+
stamp(message) {
|
|
50
|
+
if (requestedAt === undefined)
|
|
51
|
+
return;
|
|
52
|
+
const windowStart = reasonedBeforeFirstDelta(message, thinkingStreamed) ? startedMs : firstDeltaMs;
|
|
53
|
+
const generationMs = windowStart !== undefined && lastDeltaMs !== undefined && lastDeltaMs > windowStart
|
|
54
|
+
? Math.round(lastDeltaMs - windowStart)
|
|
55
|
+
: undefined;
|
|
56
|
+
message.timing = {
|
|
57
|
+
requestedAt,
|
|
58
|
+
...(firstTokenAt === undefined ? {} : { firstTokenAt }),
|
|
59
|
+
...(lastTokenAt === undefined ? {} : { lastTokenAt }),
|
|
60
|
+
endedAt: Math.max(Date.now(), lastTokenAt ?? requestedAt),
|
|
61
|
+
...(generationMs === undefined ? {} : { generationMs }),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=response-timing.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-agent-core",
|
|
3
|
-
"version": "2026.10.21-
|
|
3
|
+
"version": "2026.10.21-2",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@earendil-works/chord": "0.99.1",
|
|
65
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.21-
|
|
66
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.21-
|
|
65
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.21-2",
|
|
66
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.21-2",
|
|
67
67
|
"diff": "9.0.0",
|
|
68
68
|
"ignore": "7.0.9",
|
|
69
69
|
"typebox": "1.3.34",
|