@peakflo/agent-harness 0.2.4 → 0.2.5-rc.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/adapters/coding-agent-adapter.d.ts +6 -0
- package/dist/adapters/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi-adapter.js +137 -4
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +27 -2
- package/dist/contract.d.ts +4 -1
- package/dist/contract.js +4 -1
- package/dist/control-plane.d.ts +6 -0
- package/dist/control-plane.js +29 -0
- package/dist/harness.d.ts +14 -4
- package/dist/harness.js +156 -22
- package/dist/runtime/cold-start-steps.d.ts +11 -0
- package/dist/runtime/cold-start-steps.js +57 -0
- package/dist/runtime/session-runner.d.ts +23 -1
- package/dist/runtime/session-runner.js +241 -12
- package/dist/types.d.ts +9 -1
- package/package.json +1 -1
|
@@ -41,6 +41,10 @@ export interface McpServerConfig {
|
|
|
41
41
|
export interface SessionConfig {
|
|
42
42
|
agentId: string;
|
|
43
43
|
taskId: string;
|
|
44
|
+
/** Leased turn identifier for timing logs. */
|
|
45
|
+
turnId?: string;
|
|
46
|
+
/** Best-effort structured log sink for turn phases. */
|
|
47
|
+
coldStartLog?: (line: string) => void;
|
|
44
48
|
workspaceDir: string;
|
|
45
49
|
/**
|
|
46
50
|
* Refuse dev servers and app scaffolding in shell calls unless the turn's
|
|
@@ -182,6 +186,8 @@ export interface CodingAgentAdapter {
|
|
|
182
186
|
* not (the caller still has the transcript note either way).
|
|
183
187
|
*/
|
|
184
188
|
steerTurn?(sessionId: string, text: string, config: SessionConfig): Promise<boolean>;
|
|
189
|
+
/** A human message. Return true only once the live backend has accepted it. */
|
|
190
|
+
steerUserMessage?(sessionId: string, text: string, config: SessionConfig): Promise<boolean>;
|
|
185
191
|
/**
|
|
186
192
|
* Get current session status
|
|
187
193
|
*/
|
|
@@ -89,6 +89,7 @@ export declare class PiAdapter implements CodingAgentAdapter {
|
|
|
89
89
|
private consumeStdout;
|
|
90
90
|
private handleJsonlLine;
|
|
91
91
|
private rejectPending;
|
|
92
|
+
private refreshSteeringActivityAtIdle;
|
|
92
93
|
private handleEvent;
|
|
93
94
|
private handleExtensionUiRequest;
|
|
94
95
|
private uiRequestMessage;
|
|
@@ -110,6 +111,8 @@ export declare class PiAdapter implements CodingAgentAdapter {
|
|
|
110
111
|
* into an already-running turn must not reset them.
|
|
111
112
|
*/
|
|
112
113
|
steerTurn(sessionId: string, text: string, _config: SessionConfig): Promise<boolean>;
|
|
114
|
+
/** Resolve only when Pi emits the queued user message into the active turn. */
|
|
115
|
+
steerUserMessage(sessionId: string, text: string, _config: SessionConfig): Promise<boolean>;
|
|
113
116
|
getStatus(sessionId: string, _config: SessionConfig): Promise<SessionStatus>;
|
|
114
117
|
private settlePendingTurnError;
|
|
115
118
|
pollMessages(sessionId: string): Promise<MessagePart[]>;
|
|
@@ -21,6 +21,7 @@ const string_decoder_1 = require("string_decoder");
|
|
|
21
21
|
const util_1 = require("util");
|
|
22
22
|
const coding_agent_adapter_1 = require("./coding-agent-adapter");
|
|
23
23
|
const model_gateway_1 = require("./model-gateway");
|
|
24
|
+
const cold_start_steps_1 = require("../runtime/cold-start-steps");
|
|
24
25
|
const dev_server_guard_1 = require("../runtime/dev-server-guard");
|
|
25
26
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
26
27
|
const RPC_TIMEOUT_MS = 15_000;
|
|
@@ -29,6 +30,9 @@ const MINIMUM_PI_VERSION = [0, 80, 5];
|
|
|
29
30
|
const PI_PERMISSION_MODE_ENV = 'PEAKFLO_PI_PERMISSION_MODE';
|
|
30
31
|
exports.PI_DEV_SERVER_POLICY_ENV = 'PF_DEV_SERVER_POLICY_FILE';
|
|
31
32
|
const TERMINAL_ERROR_SETTLE_GRACE_MS = 1_000;
|
|
33
|
+
// A tool may be silent for the full progress-guard window. Turn finalization
|
|
34
|
+
// has its own short bound; this timer protects an active turn with no echo.
|
|
35
|
+
const STEER_CONSUMPTION_TIMEOUT_MS = 20 * 60_000;
|
|
32
36
|
const piInstanceIdCache = new Map();
|
|
33
37
|
/**
|
|
34
38
|
* Most model providers cap function/tool `name` at 64 characters. Pi forwards
|
|
@@ -227,6 +231,16 @@ function textFromContent(content) {
|
|
|
227
231
|
.map((block) => String(block.text ?? ''))
|
|
228
232
|
.join('');
|
|
229
233
|
}
|
|
234
|
+
function normalizeSteerText(text) {
|
|
235
|
+
return text.replace(/\r\n?/g, '\n').trim();
|
|
236
|
+
}
|
|
237
|
+
function safeSteeringError(error) {
|
|
238
|
+
// Pi may include the submitted prompt in an RPC error. Never log the error
|
|
239
|
+
// text here; even URL and token redaction cannot remove arbitrary user data.
|
|
240
|
+
return error instanceof Error && /timed out/i.test(error.message)
|
|
241
|
+
? 'rpc-timeout'
|
|
242
|
+
: 'rpc-error';
|
|
243
|
+
}
|
|
230
244
|
function resultText(result) {
|
|
231
245
|
if (!result || typeof result !== 'object')
|
|
232
246
|
return '';
|
|
@@ -470,6 +484,8 @@ class PiAdapter {
|
|
|
470
484
|
status: 'idle',
|
|
471
485
|
lastError: null,
|
|
472
486
|
pending: new Map(),
|
|
487
|
+
pendingSteers: [],
|
|
488
|
+
steeringSupported: true,
|
|
473
489
|
parts: [],
|
|
474
490
|
allMessages: [],
|
|
475
491
|
stdoutBuffer: '',
|
|
@@ -501,6 +517,17 @@ class PiAdapter {
|
|
|
501
517
|
};
|
|
502
518
|
}
|
|
503
519
|
async applySelection(session, config) {
|
|
520
|
+
// An older Pi can reject this RPC. Keep ordinary turns working and let the
|
|
521
|
+
// control plane queue any steer that this process cannot consume.
|
|
522
|
+
try {
|
|
523
|
+
await this.command(session, { type: 'set_steering_mode', mode: 'all' });
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
console.warn(`[PiAdapter/${session.id}] Pi did not accept steering mode; this session will queue new messages: ${safeSteeringError(error)}`);
|
|
527
|
+
// We cannot prove that Pi applied the mode after a timeout. Treat both
|
|
528
|
+
// explicit rejection and transport failure as a queue fallback.
|
|
529
|
+
session.steeringSupported = false;
|
|
530
|
+
}
|
|
504
531
|
if (config.model && config.model !== 'default') {
|
|
505
532
|
const { provider, modelId } = this.splitModel(config.model);
|
|
506
533
|
if (!provider || !modelId) {
|
|
@@ -516,10 +543,20 @@ class PiAdapter {
|
|
|
516
543
|
}
|
|
517
544
|
}
|
|
518
545
|
async spawnSession(config, resumeId) {
|
|
546
|
+
const steps = config.turnId
|
|
547
|
+
? new cold_start_steps_1.ColdStartSteps(config.taskId, config.turnId, (line) => {
|
|
548
|
+
console.log(line);
|
|
549
|
+
config.coldStartLog?.(line);
|
|
550
|
+
})
|
|
551
|
+
: null;
|
|
519
552
|
const executable = await this.findPiExecutable();
|
|
520
553
|
const gateway = this.installPeakfloGateway();
|
|
554
|
+
const endMcpConfig = steps?.begin('mcp_config_write');
|
|
521
555
|
const mcpConfigPath = this.buildMcpConfig(config);
|
|
556
|
+
endMcpConfig?.();
|
|
557
|
+
const endSkills = steps?.begin('skills_install');
|
|
522
558
|
const skills = this.buildSkillFiles(config);
|
|
559
|
+
endSkills?.();
|
|
523
560
|
const permissionExtension = this.installPermissionExtension();
|
|
524
561
|
const args = [
|
|
525
562
|
'--mode',
|
|
@@ -564,7 +601,9 @@ class PiAdapter {
|
|
|
564
601
|
this.sessions.set(tempId, session);
|
|
565
602
|
this.attachProcess(session);
|
|
566
603
|
try {
|
|
604
|
+
const endPiReady = steps?.begin('pi_process_ready');
|
|
567
605
|
const state = await this.command(session, { type: 'get_state' });
|
|
606
|
+
endPiReady?.();
|
|
568
607
|
const realId = typeof state.data?.sessionFile === 'string'
|
|
569
608
|
? state.data.sessionFile
|
|
570
609
|
: typeof state.data?.sessionId === 'string'
|
|
@@ -656,12 +695,41 @@ class PiAdapter {
|
|
|
656
695
|
}
|
|
657
696
|
}
|
|
658
697
|
rejectPending(session, error) {
|
|
698
|
+
for (const steer of session.pendingSteers.splice(0))
|
|
699
|
+
steer.resolve(false);
|
|
659
700
|
for (const request of session.pending.values()) {
|
|
660
701
|
clearTimeout(request.timer);
|
|
661
702
|
request.reject(error);
|
|
662
703
|
}
|
|
663
704
|
session.pending.clear();
|
|
664
705
|
}
|
|
706
|
+
async refreshSteeringActivityAtIdle(session) {
|
|
707
|
+
const observedTurn = session.turn;
|
|
708
|
+
try {
|
|
709
|
+
const state = await this.command(session, { type: 'get_state' });
|
|
710
|
+
if (session.turn !== observedTurn ||
|
|
711
|
+
session.status === 'error' ||
|
|
712
|
+
session.closing)
|
|
713
|
+
return;
|
|
714
|
+
if (Number(state.data?.pendingMessageCount ?? 0) > 0 ||
|
|
715
|
+
state.data?.isStreaming === true) {
|
|
716
|
+
session.status = 'busy';
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
session.status = 'idle';
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
console.warn(`[PiAdapter/${session.id}] Could not inspect steering queue at idle: ${safeSteeringError(error)}`);
|
|
724
|
+
if (session.turn === observedTurn &&
|
|
725
|
+
session.status !== 'error' &&
|
|
726
|
+
!session.closing)
|
|
727
|
+
session.status = 'idle';
|
|
728
|
+
}
|
|
729
|
+
// An empty queue is not proof of non-consumption: Pi can remove a message
|
|
730
|
+
// just before emitting message_start. The turn close requeues unconfirmed
|
|
731
|
+
// messages after its bounded wait.
|
|
732
|
+
}
|
|
665
733
|
handleEvent(session, event) {
|
|
666
734
|
if (event.type === 'response') {
|
|
667
735
|
const response = event;
|
|
@@ -715,10 +783,17 @@ class PiAdapter {
|
|
|
715
783
|
break;
|
|
716
784
|
case 'agent_settled': {
|
|
717
785
|
this.cancelPendingUiRequests(session, 'This request is no longer active.');
|
|
718
|
-
if (session.pendingTurnError)
|
|
786
|
+
if (session.pendingTurnError) {
|
|
719
787
|
this.settlePendingTurnError(session);
|
|
720
|
-
|
|
721
|
-
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
session.status = session.pendingSteers.length ? 'busy' : 'idle';
|
|
791
|
+
// Pi can settle after a tool even with a steer still queued.
|
|
792
|
+
// Keep the session busy until this state read completes.
|
|
793
|
+
if (session.pendingSteers.length) {
|
|
794
|
+
void this.refreshSteeringActivityAtIdle(session);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
722
797
|
session.promptMayBeCommandOnly = false;
|
|
723
798
|
break;
|
|
724
799
|
}
|
|
@@ -726,6 +801,16 @@ class PiAdapter {
|
|
|
726
801
|
const message = event.message;
|
|
727
802
|
if (message?.role === 'assistant')
|
|
728
803
|
session.assistantMessageOrdinal++;
|
|
804
|
+
if (message?.role === 'user') {
|
|
805
|
+
const text = textFromContent(message.content);
|
|
806
|
+
// Pi gives no steer ID on this event. Match the oldest equal text.
|
|
807
|
+
// This assumes an equal user event came from Pi's prompt queue; an
|
|
808
|
+
// unrelated equal user event could confirm the steer early.
|
|
809
|
+
const index = session.pendingSteers.findIndex((steer) => normalizeSteerText(steer.text) === normalizeSteerText(text));
|
|
810
|
+
if (index >= 0) {
|
|
811
|
+
session.pendingSteers.splice(index, 1)[0].resolve(true);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
729
814
|
break;
|
|
730
815
|
}
|
|
731
816
|
case 'message_update':
|
|
@@ -893,6 +978,11 @@ class PiAdapter {
|
|
|
893
978
|
handleMessageUpdate(session, update) {
|
|
894
979
|
if (!update)
|
|
895
980
|
return;
|
|
981
|
+
if ((update.type === 'text_delta' || update.type === 'thinking_delta') &&
|
|
982
|
+
String(update.delta || '')) {
|
|
983
|
+
session.firstTokenEnd?.();
|
|
984
|
+
session.firstTokenEnd = undefined;
|
|
985
|
+
}
|
|
896
986
|
const index = String(update.contentIndex ?? 0);
|
|
897
987
|
const key = `${session.projectionId}:${session.turn}:${session.assistantMessageOrdinal}:${index}`;
|
|
898
988
|
if (update.type === 'text_delta') {
|
|
@@ -1050,6 +1140,12 @@ class PiAdapter {
|
|
|
1050
1140
|
session.lastError = null;
|
|
1051
1141
|
session.sawAgentActivity = false;
|
|
1052
1142
|
session.promptMayBeCommandOnly = message.trimStart().startsWith('/');
|
|
1143
|
+
session.firstTokenEnd = config.turnId
|
|
1144
|
+
? new cold_start_steps_1.ColdStartSteps(config.taskId, config.turnId, (line) => {
|
|
1145
|
+
console.log(line);
|
|
1146
|
+
config.coldStartLog?.(line);
|
|
1147
|
+
}).begin('first_model_token')
|
|
1148
|
+
: undefined;
|
|
1053
1149
|
this.sendRecord(session, {
|
|
1054
1150
|
type: 'prompt',
|
|
1055
1151
|
message,
|
|
@@ -1083,8 +1179,45 @@ class PiAdapter {
|
|
|
1083
1179
|
});
|
|
1084
1180
|
return true;
|
|
1085
1181
|
}
|
|
1086
|
-
catch {
|
|
1182
|
+
catch (error) {
|
|
1183
|
+
console.warn(`[PiAdapter/${session.id}] Pi could not accept a steer message: ${safeSteeringError(error)}`);
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/** Resolve only when Pi emits the queued user message into the active turn. */
|
|
1188
|
+
async steerUserMessage(sessionId, text, _config) {
|
|
1189
|
+
const session = this.sessions.get(sessionId);
|
|
1190
|
+
if (!session ||
|
|
1191
|
+
session.status !== 'busy' ||
|
|
1192
|
+
session.steeringSupported === false ||
|
|
1193
|
+
!text)
|
|
1087
1194
|
return false;
|
|
1195
|
+
let resolveSeen;
|
|
1196
|
+
const seen = new Promise((resolve) => {
|
|
1197
|
+
resolveSeen = resolve;
|
|
1198
|
+
});
|
|
1199
|
+
const waiter = { text, resolve: resolveSeen };
|
|
1200
|
+
session.pendingSteers.push(waiter);
|
|
1201
|
+
// A timeout can requeue text Pi already consumed if its user event was
|
|
1202
|
+
// lost. Durable exact-once recovery is tracked by task_1791540809105_ual6uq3.
|
|
1203
|
+
const timer = setTimeout(() => resolveSeen(false), STEER_CONSUMPTION_TIMEOUT_MS);
|
|
1204
|
+
timer.unref?.();
|
|
1205
|
+
try {
|
|
1206
|
+
await this.command(session, {
|
|
1207
|
+
type: 'steer',
|
|
1208
|
+
message: text
|
|
1209
|
+
});
|
|
1210
|
+
return await seen;
|
|
1211
|
+
}
|
|
1212
|
+
catch (error) {
|
|
1213
|
+
console.warn(`[PiAdapter/${session.id}] Pi could not accept a steer message: ${safeSteeringError(error)}`);
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
finally {
|
|
1217
|
+
clearTimeout(timer);
|
|
1218
|
+
const index = session.pendingSteers.indexOf(waiter);
|
|
1219
|
+
if (index >= 0)
|
|
1220
|
+
session.pendingSteers.splice(index, 1);
|
|
1088
1221
|
}
|
|
1089
1222
|
}
|
|
1090
1223
|
async getStatus(sessionId, _config) {
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { RunnerCapabilities } from './types';
|
|
|
32
32
|
* `knowledge_files_list` / `knowledge_file_download` (P9).
|
|
33
33
|
*/
|
|
34
34
|
export declare const HARNESS_FEATURES: readonly ["spaceFiles"];
|
|
35
|
+
/** Steering mode was verified with Pi 0.84.3. Older Pi keeps the queue path. */
|
|
36
|
+
export declare function supportsPiSteering(version: string | null): boolean;
|
|
35
37
|
export interface ProbeResult {
|
|
36
38
|
installed: boolean;
|
|
37
39
|
version: string | null;
|
package/dist/capabilities.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
28
|
exports.HARNESS_FEATURES = void 0;
|
|
29
|
+
exports.supportsPiSteering = supportsPiSteering;
|
|
29
30
|
exports.probe = probe;
|
|
30
31
|
exports.probeCapabilities = probeCapabilities;
|
|
31
32
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -42,6 +43,25 @@ const PROBE_TIMEOUT_MS = 10_000;
|
|
|
42
43
|
* `knowledge_files_list` / `knowledge_file_download` (P9).
|
|
43
44
|
*/
|
|
44
45
|
exports.HARNESS_FEATURES = ['spaceFiles'];
|
|
46
|
+
/** Steering mode was verified with Pi 0.84.3. Older Pi keeps the queue path. */
|
|
47
|
+
function supportsPiSteering(version) {
|
|
48
|
+
if (!version)
|
|
49
|
+
return false;
|
|
50
|
+
// Only a stable release was verified. A prerelease or custom build can
|
|
51
|
+
// change the RPC contract even when its base version meets the minimum.
|
|
52
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
53
|
+
if (!match)
|
|
54
|
+
return false;
|
|
55
|
+
const parts = match.slice(1, 4).map(Number);
|
|
56
|
+
const minimum = [0, 84, 3];
|
|
57
|
+
for (let index = 0; index < minimum.length; index++) {
|
|
58
|
+
if (parts[index] > minimum[index])
|
|
59
|
+
return true;
|
|
60
|
+
if (parts[index] < minimum[index])
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
45
65
|
/**
|
|
46
66
|
* The coding-agent backends this harness knows how to drive, and the command
|
|
47
67
|
* that reports each one's version.
|
|
@@ -105,7 +125,7 @@ function probe(command, args = ['--version']) {
|
|
|
105
125
|
const raw = String(stdout).trim();
|
|
106
126
|
// Same expression as detect.js: pick the first version-like token, and
|
|
107
127
|
// fall back to the first line when a tool prints something exotic.
|
|
108
|
-
const match = raw.match(/(\d+\.\d+[\w
|
|
128
|
+
const match = raw.match(/(\d+\.\d+[\w.+-]*)/);
|
|
109
129
|
finish({
|
|
110
130
|
installed: true,
|
|
111
131
|
version: match ? match[1] : raw.split('\n')[0] || null
|
|
@@ -165,7 +185,12 @@ async function probeCapabilities(options = {}) {
|
|
|
165
185
|
git: git.installed,
|
|
166
186
|
docker: docker.installed,
|
|
167
187
|
maxConcurrentSessions: options.maxConcurrentSessions ?? 1,
|
|
168
|
-
features: [
|
|
188
|
+
features: [
|
|
189
|
+
...exports.HARNESS_FEATURES,
|
|
190
|
+
...(backends.some((backend) => backend.name === 'pi' && supportsPiSteering(backend.version))
|
|
191
|
+
? ['steerMessages']
|
|
192
|
+
: [])
|
|
193
|
+
],
|
|
169
194
|
labels: options.labels
|
|
170
195
|
};
|
|
171
196
|
}
|
package/dist/contract.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* needs to tell such a harness apart, which it does by refusing it
|
|
32
32
|
* rather than silently serving a value that would misbehave.
|
|
33
33
|
*/
|
|
34
|
-
export declare const PROTOCOL_VERSION =
|
|
34
|
+
export declare const PROTOCOL_VERSION = 3;
|
|
35
35
|
/**
|
|
36
36
|
* The floor a harness must meet to be served `maxTurnDurationMs: null`.
|
|
37
37
|
*
|
|
@@ -178,6 +178,8 @@ export declare const ROUTES: {
|
|
|
178
178
|
readonly register: () => string;
|
|
179
179
|
/** Bearer: runner credential. 30s period. */
|
|
180
180
|
readonly heartbeat: (runnerId: string) => string;
|
|
181
|
+
/** Bearer: runner credential. Best-effort timing event; no transcript data. */
|
|
182
|
+
readonly coldStart: (runnerId: string) => string;
|
|
181
183
|
/**
|
|
182
184
|
* Bearer: runner credential. Bounded long-poll: 204 idle, 200 with work.
|
|
183
185
|
*
|
|
@@ -194,6 +196,7 @@ export declare const ROUTES: {
|
|
|
194
196
|
readonly lease: (runnerId: string, waitSeconds?: number, protocolVersion?: number) => string;
|
|
195
197
|
/** Bearer: runner credential. Visibility-timeout renewal. */
|
|
196
198
|
readonly leaseRenew: (runnerId: string, leaseId: string) => string;
|
|
199
|
+
readonly steerAck: (runnerId: string, leaseId: string, turnId: string) => string;
|
|
197
200
|
/** Bearer: runner credential. Metadata of the knowledge files this turn may read. */
|
|
198
201
|
readonly knowledgeFiles: (runnerId: string, leaseId: string) => string;
|
|
199
202
|
/** Bearer: runner credential. A short-lived download URL for one knowledge file. */
|
package/dist/contract.js
CHANGED
|
@@ -42,7 +42,7 @@ exports.isPersistablePart = isPersistablePart;
|
|
|
42
42
|
* needs to tell such a harness apart, which it does by refusing it
|
|
43
43
|
* rather than silently serving a value that would misbehave.
|
|
44
44
|
*/
|
|
45
|
-
exports.PROTOCOL_VERSION =
|
|
45
|
+
exports.PROTOCOL_VERSION = 3;
|
|
46
46
|
/**
|
|
47
47
|
* The floor a harness must meet to be served `maxTurnDurationMs: null`.
|
|
48
48
|
*
|
|
@@ -215,6 +215,8 @@ exports.ROUTES = {
|
|
|
215
215
|
register: () => '/api/runners/register',
|
|
216
216
|
/** Bearer: runner credential. 30s period. */
|
|
217
217
|
heartbeat: (runnerId) => `/api/runners/${encodeURIComponent(runnerId)}/heartbeat`,
|
|
218
|
+
/** Bearer: runner credential. Best-effort timing event; no transcript data. */
|
|
219
|
+
coldStart: (runnerId) => `/api/runners/${encodeURIComponent(runnerId)}/cold-start`,
|
|
218
220
|
/**
|
|
219
221
|
* Bearer: runner credential. Bounded long-poll: 204 idle, 200 with work.
|
|
220
222
|
*
|
|
@@ -234,6 +236,7 @@ exports.ROUTES = {
|
|
|
234
236
|
},
|
|
235
237
|
/** Bearer: runner credential. Visibility-timeout renewal. */
|
|
236
238
|
leaseRenew: (runnerId, leaseId) => `/api/runners/${encodeURIComponent(runnerId)}/lease/${encodeURIComponent(leaseId)}/renew`,
|
|
239
|
+
steerAck: (runnerId, leaseId, turnId) => `/api/runners/${encodeURIComponent(runnerId)}/lease/${encodeURIComponent(leaseId)}/steer/${encodeURIComponent(turnId)}/ack`,
|
|
237
240
|
/** Bearer: runner credential. Metadata of the knowledge files this turn may read. */
|
|
238
241
|
knowledgeFiles: (runnerId, leaseId) => `/api/runners/${encodeURIComponent(runnerId)}/lease/${encodeURIComponent(leaseId)}/knowledge/files`,
|
|
239
242
|
/** Bearer: runner credential. A short-lived download URL for one knowledge file. */
|
package/dist/control-plane.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export declare class ControlPlaneClient {
|
|
|
81
81
|
/** Used only for a private IPC handoff to the baked sandbox supervisor. */
|
|
82
82
|
getRunnerCredential(): string | null;
|
|
83
83
|
getRunnerId(): string | null;
|
|
84
|
+
/** Diagnostics must never delay a turn. The caller handles any failure. */
|
|
85
|
+
reportColdStart(line: string): Promise<void>;
|
|
84
86
|
/**
|
|
85
87
|
* Give the lease-renewal worker the minimum information it needs.
|
|
86
88
|
*
|
|
@@ -129,6 +131,7 @@ export declare class ControlPlaneClient {
|
|
|
129
131
|
* 25s poll under a 15s timeout would abort every single call.
|
|
130
132
|
*/
|
|
131
133
|
lease(waitSeconds?: number): Promise<LeaseResponse | null>;
|
|
134
|
+
acknowledgeLease(leaseId: string): Promise<LeaseRenewResponse>;
|
|
132
135
|
renewLease(leaseId: string, status?: RunnerStatus, githubTokenExpiresAt?: string, modelTokenExpiresAt?: string): Promise<LeaseRenewResponse>;
|
|
133
136
|
/** Metadata of the knowledge files the lease's turn may read. Never a URL. */
|
|
134
137
|
knowledgeFiles(leaseId: string, datastoreId?: string): Promise<KnowledgeFilesResponse>;
|
|
@@ -147,6 +150,9 @@ export declare class ControlPlaneClient {
|
|
|
147
150
|
* idempotent.
|
|
148
151
|
*/
|
|
149
152
|
ingest(sessionId: string, request: Omit<IngestRequest, 'protocolVersion'>): Promise<IngestResponse>;
|
|
153
|
+
acknowledgeSteer(sessionId: string, leaseId: string, turnId: string, consumed: boolean): Promise<{
|
|
154
|
+
delivery: 'steered' | 'queued';
|
|
155
|
+
}>;
|
|
150
156
|
/**
|
|
151
157
|
* Read the session's model-facing conversation back from the control plane.
|
|
152
158
|
*
|
package/dist/control-plane.js
CHANGED
|
@@ -114,6 +114,18 @@ class ControlPlaneClient {
|
|
|
114
114
|
getRunnerId() {
|
|
115
115
|
return this.runnerId;
|
|
116
116
|
}
|
|
117
|
+
/** Diagnostics must never delay a turn. The caller handles any failure. */
|
|
118
|
+
async reportColdStart(line) {
|
|
119
|
+
if (!this.runnerId || !this.runnerCredential)
|
|
120
|
+
return;
|
|
121
|
+
await this.send({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
path: contract_1.ROUTES.coldStart(this.runnerId),
|
|
124
|
+
token: this.runnerCredential,
|
|
125
|
+
body: JSON.parse(line),
|
|
126
|
+
timeoutMs: 2_000
|
|
127
|
+
});
|
|
128
|
+
}
|
|
117
129
|
/**
|
|
118
130
|
* Give the lease-renewal worker the minimum information it needs.
|
|
119
131
|
*
|
|
@@ -248,6 +260,15 @@ class ControlPlaneClient {
|
|
|
248
260
|
/* ------------------------------------------------------------------ */
|
|
249
261
|
/* 4. lease renew */
|
|
250
262
|
/* ------------------------------------------------------------------ */
|
|
263
|
+
async acknowledgeLease(leaseId) {
|
|
264
|
+
const runnerId = this.requireRunnerId();
|
|
265
|
+
return this.send({
|
|
266
|
+
method: 'POST',
|
|
267
|
+
path: contract_1.ROUTES.leaseRenew(runnerId, leaseId),
|
|
268
|
+
token: this.requireCredential(),
|
|
269
|
+
body: { protocolVersion: contract_1.PROTOCOL_VERSION, ackOnly: true }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
251
272
|
async renewLease(leaseId, status, githubTokenExpiresAt, modelTokenExpiresAt) {
|
|
252
273
|
const runnerId = this.requireRunnerId();
|
|
253
274
|
return this.send({
|
|
@@ -308,6 +329,14 @@ class ControlPlaneClient {
|
|
|
308
329
|
body: { protocolVersion: contract_1.PROTOCOL_VERSION, ...request }
|
|
309
330
|
}));
|
|
310
331
|
}
|
|
332
|
+
async acknowledgeSteer(sessionId, leaseId, turnId, consumed) {
|
|
333
|
+
return this.withRetry('steer-ack', () => this.send({
|
|
334
|
+
method: 'POST',
|
|
335
|
+
path: contract_1.ROUTES.steerAck(this.requireRunnerId(), leaseId, turnId),
|
|
336
|
+
token: this.requireCredential(),
|
|
337
|
+
body: { protocolVersion: contract_1.PROTOCOL_VERSION, sessionId, consumed }
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
311
340
|
/* ------------------------------------------------------------------ */
|
|
312
341
|
/* 5b. session history — the runner's READ path */
|
|
313
342
|
/* ------------------------------------------------------------------ */
|
package/dist/harness.d.ts
CHANGED
|
@@ -174,10 +174,15 @@ export declare class Harness {
|
|
|
174
174
|
private releaseGitHubGuard;
|
|
175
175
|
/** sessionId -> the controller that aborts its turn. */
|
|
176
176
|
private readonly active;
|
|
177
|
+
private readonly activeLeaseIds;
|
|
177
178
|
/** Renewal workers that must receive a credential rotation in flight. */
|
|
178
179
|
private readonly activeRenewals;
|
|
179
180
|
/** commandIds already applied. `SessionCommand` is idempotent by contract. */
|
|
180
181
|
private readonly appliedCommands;
|
|
182
|
+
/** Prune lease-bound command IDs when their session turn ends. */
|
|
183
|
+
private readonly appliedCommandsBySession;
|
|
184
|
+
/** A steer can arrive on renewal while a leased runner is still starting. */
|
|
185
|
+
private readonly deferredSteers;
|
|
181
186
|
/** Session-end cleanup waits for an active turn to save its dirty work. */
|
|
182
187
|
private readonly pendingWorkspaceReleases;
|
|
183
188
|
/**
|
|
@@ -283,14 +288,19 @@ export declare class Harness {
|
|
|
283
288
|
private modelsNeedRefresh;
|
|
284
289
|
private scheduleHeartbeat;
|
|
285
290
|
private sendHeartbeat;
|
|
291
|
+
/** A bad command must not stop the others or fail a transcript ingest. */
|
|
292
|
+
private applyCommandsSafely;
|
|
293
|
+
private forgetAppliedCommand;
|
|
294
|
+
private clearAppliedCommandsForSession;
|
|
286
295
|
/**
|
|
287
|
-
* Apply a command that rode back on a heartbeat.
|
|
296
|
+
* Apply a command that rode back on a heartbeat or ingest response.
|
|
288
297
|
*
|
|
289
|
-
* Idempotent by `commandId`, because the
|
|
290
|
-
*
|
|
291
|
-
* know, since it cannot dial in and ask.
|
|
298
|
+
* Idempotent by `commandId`, because the control plane may repeat a command
|
|
299
|
+
* until it observes the effect; it cannot dial the harness to ask.
|
|
292
300
|
*/
|
|
293
301
|
private applyCommand;
|
|
302
|
+
/** Deliver commands received while the leased runner was still starting. */
|
|
303
|
+
private flushDeferredSteers;
|
|
294
304
|
private releaseSessionWorkspace;
|
|
295
305
|
/**
|
|
296
306
|
* A user's answer did not reach the backend. Do the two things that keep it
|