@cursor/july 0.1.28 → 0.1.29
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/bin/agent-serve.js +20 -2
- package/dist/channels/slack/slack-channel.d.ts +19 -1
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +29 -0
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.CqjrYaT2.js → app.QdunVQKg.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DGe61XHA.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.ag-MwUel.js → VPLocalSearchBox.B6EpUXYf.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BS-DJWat.js → theme.BOTJVqh7.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/cloud-turn-cost.d.ts +34 -0
- package/dist/internal/cloud-turn-cost.d.ts.map +1 -0
- package/dist/internal/cloud-turn-cost.js +106 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/event-log.d.ts +6 -0
- package/dist/internal/event-log.d.ts.map +1 -1
- package/dist/internal/event-log.js +16 -0
- package/dist/internal/host-platforms.d.ts +13 -0
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +25 -16
- package/dist/internal/http-channel.d.ts.map +1 -1
- package/dist/internal/http-channel.js +3 -1
- package/dist/internal/local-control-plane.d.ts +25 -0
- package/dist/internal/local-control-plane.d.ts.map +1 -0
- package/dist/internal/local-control-plane.js +172 -0
- package/dist/internal/platform-schedule-sync.d.ts +17 -0
- package/dist/internal/platform-schedule-sync.d.ts.map +1 -0
- package/dist/internal/platform-schedule-sync.js +72 -0
- package/dist/internal/platform-timers.d.ts +28 -0
- package/dist/internal/platform-timers.d.ts.map +1 -0
- package/dist/internal/platform-timers.js +120 -0
- package/dist/internal/reminder-runner.d.ts +16 -12
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +112 -61
- package/dist/internal/reminder-store.d.ts +5 -4
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +5 -4
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +44 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +105 -11
- package/dist/internal/session-engine.d.ts +22 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +110 -4
- package/dist/internal/storage-coordinator.d.ts +4 -15
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +4 -30
- package/dist/playground/assets/index-CquRB-l0.js +85 -0
- package/dist/playground/assets/{index-Bul1WfrB.css → index-Dj0bWpkn.css} +1 -1
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +0 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +0 -3
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bin/agent-serve.ts +21 -2
- package/src/channels/slack/slack-channel.ts +46 -0
- package/src/internal/cloud-turn-cost.ts +158 -0
- package/src/internal/discovery.ts +1 -0
- package/src/internal/event-log.ts +17 -0
- package/src/internal/host-platforms.ts +40 -18
- package/src/internal/http-channel.ts +3 -1
- package/src/internal/local-control-plane.ts +188 -0
- package/src/internal/platform-schedule-sync.ts +81 -0
- package/src/internal/platform-timers.ts +173 -0
- package/src/internal/reminder-runner.ts +131 -67
- package/src/internal/reminder-store.ts +5 -4
- package/src/internal/sdk-runner.ts +49 -1
- package/src/internal/server.ts +121 -6
- package/src/internal/session-engine.ts +138 -3
- package/src/internal/storage-coordinator.ts +4 -41
- package/src/storage.ts +0 -5
- package/src/types.ts +11 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +0 -1
- package/dist/playground/assets/index-CkN74U_L.js +0 -87
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { join } from "node:path";
|
|
9
8
|
import type {
|
|
10
9
|
AuthContext,
|
|
11
10
|
ReminderCreateInput,
|
|
@@ -15,6 +14,7 @@ import type {
|
|
|
15
14
|
ReminderUntilContext,
|
|
16
15
|
} from "../types.js";
|
|
17
16
|
import { parseDuration } from "./duration.js";
|
|
17
|
+
import type { PlatformTimers } from "./platform-timers.js";
|
|
18
18
|
import {
|
|
19
19
|
defaultBusyPolicy,
|
|
20
20
|
MAX_TIMER_MS,
|
|
@@ -22,11 +22,7 @@ import {
|
|
|
22
22
|
ReminderValidationError,
|
|
23
23
|
resolveSchedule,
|
|
24
24
|
} from "./reminder-schedule.js";
|
|
25
|
-
import {
|
|
26
|
-
type ReminderBusyPolicy,
|
|
27
|
-
type ReminderRecord,
|
|
28
|
-
ReminderStore,
|
|
29
|
-
} from "./reminder-store.js";
|
|
25
|
+
import type { ReminderBusyPolicy, ReminderRecord } from "./reminder-store.js";
|
|
30
26
|
import { APP_AUTH } from "./schedule-runner.js";
|
|
31
27
|
import { SessionBusyError, type SessionEngine } from "./session-engine.js";
|
|
32
28
|
|
|
@@ -98,7 +94,7 @@ function buildFirePrompt(record: ReminderRecord): string {
|
|
|
98
94
|
}
|
|
99
95
|
|
|
100
96
|
export class ReminderRunner {
|
|
101
|
-
private readonly
|
|
97
|
+
private readonly records = new Map<string, ReminderRecord>();
|
|
102
98
|
private readonly handlers = new Map<string, LiveHandlers>();
|
|
103
99
|
private readonly timers = new Map<string, NodeJS.Timeout>();
|
|
104
100
|
private readonly firing = new Set<string>();
|
|
@@ -108,28 +104,70 @@ export class ReminderRunner {
|
|
|
108
104
|
|
|
109
105
|
constructor(
|
|
110
106
|
private readonly engine: SessionEngine,
|
|
111
|
-
private readonly logger: (line: string) => void = () => {}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
107
|
+
private readonly logger: (line: string) => void = () => {},
|
|
108
|
+
private readonly platformTimers?: PlatformTimers
|
|
109
|
+
) {}
|
|
115
110
|
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Load reminder documents from the control plane into memory. Does not
|
|
113
|
+
* arm in-process timers — the CP owns clocks.
|
|
114
|
+
*/
|
|
115
|
+
async hydrate(): Promise<void> {
|
|
116
|
+
if (this.platformTimers === undefined) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let remote: ReminderRecord[] = [];
|
|
120
|
+
try {
|
|
121
|
+
remote = await this.platformTimers.listReminders();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
this.logger(
|
|
124
|
+
`[agentkit] reminders: control-plane list failed: ${
|
|
125
|
+
error instanceof Error ? error.message : String(error)
|
|
126
|
+
}`
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.records.clear();
|
|
131
|
+
for (const record of remote) {
|
|
132
|
+
this.records.set(record.id, record);
|
|
133
|
+
}
|
|
134
|
+
let disarmedOrphan = 0;
|
|
135
|
+
for (const record of [...this.records.values()]) {
|
|
136
|
+
if (record.status !== "armed") {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
141
|
+
this.handlers.get(record.id) === undefined
|
|
142
|
+
) {
|
|
143
|
+
await this.disarm(record, "handler_lost_on_restart");
|
|
144
|
+
disarmedOrphan += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (remote.length > 0 || disarmedOrphan > 0) {
|
|
148
|
+
this.logger(
|
|
149
|
+
`[agentkit] reminders: hydrated ${remote.length} reminder(s) from control plane` +
|
|
150
|
+
(disarmedOrphan > 0
|
|
151
|
+
? `; disarmed ${disarmedOrphan} without live handlers`
|
|
152
|
+
: "")
|
|
153
|
+
);
|
|
154
|
+
}
|
|
118
155
|
}
|
|
119
156
|
|
|
120
|
-
/** Start deadline timers for armed reminders (
|
|
157
|
+
/** Start deadline timers for armed reminders (tests / no control plane). */
|
|
121
158
|
async start(): Promise<void> {
|
|
122
159
|
if (this.started) return;
|
|
123
160
|
this.started = true;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
await this.hydrate();
|
|
162
|
+
if (this.platformTimers !== undefined) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const armed = [...this.records.values()].filter(
|
|
166
|
+
(r) => r.status === "armed"
|
|
167
|
+
);
|
|
128
168
|
let restored = 0;
|
|
129
169
|
let disarmedOrphan = 0;
|
|
130
170
|
for (const record of armed) {
|
|
131
|
-
// Run / function-until handlers are in-memory only. After restart they
|
|
132
|
-
// are gone — disarm rather than falling back to a generic prompt fire.
|
|
133
171
|
if (
|
|
134
172
|
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
135
173
|
this.handlers.get(record.id) === undefined
|
|
@@ -151,43 +189,52 @@ export class ReminderRunner {
|
|
|
151
189
|
}
|
|
152
190
|
}
|
|
153
191
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*/
|
|
158
|
-
private async hydrateFromStorage(): Promise<void> {
|
|
159
|
-
const storage = this.engine.storage;
|
|
160
|
-
if (storage === undefined || !storage.canBulkRestore) {
|
|
161
|
-
return;
|
|
192
|
+
private async persist(record: ReminderRecord): Promise<void> {
|
|
193
|
+
if (this.platformTimers !== undefined) {
|
|
194
|
+
await this.platformTimers.putReminder(record);
|
|
162
195
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
196
|
+
this.records.set(record.id, record);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async getRecord(id: string): Promise<ReminderRecord | undefined> {
|
|
200
|
+
if (this.platformTimers !== undefined) {
|
|
201
|
+
try {
|
|
202
|
+
const remote = await this.platformTimers.getReminder(id);
|
|
203
|
+
if (remote === undefined) {
|
|
204
|
+
this.records.delete(id);
|
|
205
|
+
return undefined;
|
|
170
206
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
this.records.set(id, remote);
|
|
208
|
+
return remote;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.logger(
|
|
211
|
+
`[agentkit] reminders: control-plane get failed for ${id}: ${
|
|
212
|
+
error instanceof Error ? error.message : String(error)
|
|
213
|
+
}`
|
|
214
|
+
);
|
|
215
|
+
return this.records.get(id);
|
|
174
216
|
}
|
|
175
|
-
} catch (error) {
|
|
176
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
-
this.logger(`[agentkit] reminders: storage hydrate failed: ${message}`);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (hydrated > 0) {
|
|
181
|
-
this.logger(
|
|
182
|
-
`[agentkit] reminders: hydrated ${hydrated} reminder(s) from storage`
|
|
183
|
-
);
|
|
184
217
|
}
|
|
218
|
+
return this.records.get(id);
|
|
185
219
|
}
|
|
186
220
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
private async allRecords(): Promise<ReminderRecord[]> {
|
|
222
|
+
if (this.platformTimers !== undefined) {
|
|
223
|
+
try {
|
|
224
|
+
const remote = await this.platformTimers.listReminders();
|
|
225
|
+
this.records.clear();
|
|
226
|
+
for (const record of remote) {
|
|
227
|
+
this.records.set(record.id, record);
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
this.logger(
|
|
231
|
+
`[agentkit] reminders: control-plane list failed: ${
|
|
232
|
+
error instanceof Error ? error.message : String(error)
|
|
233
|
+
}`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return [...this.records.values()];
|
|
191
238
|
}
|
|
192
239
|
|
|
193
240
|
stop(): void {
|
|
@@ -256,7 +303,7 @@ export class ReminderRunner {
|
|
|
256
303
|
run: input.run,
|
|
257
304
|
untilFn,
|
|
258
305
|
});
|
|
259
|
-
if (this.started) {
|
|
306
|
+
if (this.started && this.platformTimers === undefined) {
|
|
260
307
|
this.armTimer(record);
|
|
261
308
|
}
|
|
262
309
|
|
|
@@ -268,7 +315,7 @@ export class ReminderRunner {
|
|
|
268
315
|
purpose?: string;
|
|
269
316
|
continuationToken?: string;
|
|
270
317
|
}): Promise<ReminderInfo[]> {
|
|
271
|
-
const all = await this.
|
|
318
|
+
const all = await this.allRecords();
|
|
272
319
|
return all
|
|
273
320
|
.filter((r) => {
|
|
274
321
|
if (filter?.status !== undefined && r.status !== filter.status) {
|
|
@@ -301,7 +348,7 @@ export class ReminderRunner {
|
|
|
301
348
|
id: string,
|
|
302
349
|
reason = "explicit_cancel"
|
|
303
350
|
): Promise<ReminderInfo | undefined> {
|
|
304
|
-
const record = await this.
|
|
351
|
+
const record = await this.getRecord(id);
|
|
305
352
|
if (record === undefined) {
|
|
306
353
|
return undefined;
|
|
307
354
|
}
|
|
@@ -325,7 +372,7 @@ export class ReminderRunner {
|
|
|
325
372
|
continuationToken: string,
|
|
326
373
|
reason: string
|
|
327
374
|
): Promise<number> {
|
|
328
|
-
const armed = await this.
|
|
375
|
+
const armed = (await this.allRecords()).filter((r) => r.status === "armed");
|
|
329
376
|
let n = 0;
|
|
330
377
|
for (const record of armed) {
|
|
331
378
|
if (record.continuationToken === continuationToken) {
|
|
@@ -347,7 +394,7 @@ export class ReminderRunner {
|
|
|
347
394
|
oldToken: string,
|
|
348
395
|
newToken: string
|
|
349
396
|
): Promise<number> {
|
|
350
|
-
const armed = await this.
|
|
397
|
+
const armed = (await this.allRecords()).filter((r) => r.status === "armed");
|
|
351
398
|
let n = 0;
|
|
352
399
|
for (const record of armed) {
|
|
353
400
|
if (
|
|
@@ -366,17 +413,30 @@ export class ReminderRunner {
|
|
|
366
413
|
return n;
|
|
367
414
|
}
|
|
368
415
|
|
|
369
|
-
/** Fire one reminder immediately (dev / hillclimb). */
|
|
370
|
-
async dispatch(
|
|
416
|
+
/** Fire one reminder immediately (dev / hillclimb / control plane). */
|
|
417
|
+
async dispatch(
|
|
418
|
+
reminderId: string,
|
|
419
|
+
opts: { expectedFireCount?: number } = {}
|
|
420
|
+
): Promise<{
|
|
371
421
|
reminderId: string;
|
|
372
422
|
outcome: "fired" | "noop";
|
|
373
423
|
reason?: string;
|
|
374
424
|
}> {
|
|
375
|
-
const record = await this.
|
|
425
|
+
const record = await this.getRecord(reminderId);
|
|
376
426
|
if (record === undefined) {
|
|
377
|
-
const ids = (await this.
|
|
427
|
+
const ids = (await this.allRecords()).map((r) => r.id);
|
|
378
428
|
throw new UnknownReminderError(reminderId, ids);
|
|
379
429
|
}
|
|
430
|
+
if (
|
|
431
|
+
opts.expectedFireCount !== undefined &&
|
|
432
|
+
record.fireCount !== opts.expectedFireCount
|
|
433
|
+
) {
|
|
434
|
+
return {
|
|
435
|
+
reminderId,
|
|
436
|
+
outcome: "noop",
|
|
437
|
+
reason: "stale_fire_count",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
380
440
|
const result = await this.fire(record.id, { force: true });
|
|
381
441
|
return { reminderId, ...result };
|
|
382
442
|
}
|
|
@@ -401,7 +461,7 @@ export class ReminderRunner {
|
|
|
401
461
|
// Keep recurring reminders alive after unexpected throws.
|
|
402
462
|
// One-shots stay armed so a failed wake can be retried / dispatched.
|
|
403
463
|
try {
|
|
404
|
-
const current = await this.
|
|
464
|
+
const current = await this.getRecord(record.id);
|
|
405
465
|
if (current === undefined || current.status !== "armed") return;
|
|
406
466
|
const oneShot =
|
|
407
467
|
current.scheduleKind === "delay" || current.scheduleKind === "at";
|
|
@@ -442,7 +502,7 @@ export class ReminderRunner {
|
|
|
442
502
|
}
|
|
443
503
|
this.firing.add(id);
|
|
444
504
|
try {
|
|
445
|
-
const record = await this.
|
|
505
|
+
const record = await this.getRecord(id);
|
|
446
506
|
if (record === undefined || record.status !== "armed") {
|
|
447
507
|
this.clearTimer(id);
|
|
448
508
|
return { outcome: "noop", reason: "not_armed" };
|
|
@@ -493,7 +553,7 @@ export class ReminderRunner {
|
|
|
493
553
|
return { outcome: "fired", reason: "skipped_busy" };
|
|
494
554
|
}
|
|
495
555
|
await this.whenSessionIdle(record.channelId, record.continuationToken);
|
|
496
|
-
const still = await this.
|
|
556
|
+
const still = await this.getRecord(id);
|
|
497
557
|
if (still === undefined || still.status !== "armed") {
|
|
498
558
|
return { outcome: "noop", reason: "disarmed_while_waiting" };
|
|
499
559
|
}
|
|
@@ -511,7 +571,7 @@ export class ReminderRunner {
|
|
|
511
571
|
}
|
|
512
572
|
|
|
513
573
|
// Cancel / disarm during the handler must win over reschedule.
|
|
514
|
-
const latest = await this.
|
|
574
|
+
const latest = await this.getRecord(id);
|
|
515
575
|
if (result.action === "stop") {
|
|
516
576
|
if (latest !== undefined && latest.status === "armed") {
|
|
517
577
|
await this.disarm(latest, "run_stop");
|
|
@@ -740,7 +800,7 @@ export class ReminderRunner {
|
|
|
740
800
|
_outcome: "skip" | "delivered",
|
|
741
801
|
reschedule?: { delay?: string | number; at?: Date | string }
|
|
742
802
|
): Promise<void> {
|
|
743
|
-
const current = await this.
|
|
803
|
+
const current = await this.getRecord(record.id);
|
|
744
804
|
if (current === undefined || current.status !== "armed") {
|
|
745
805
|
return;
|
|
746
806
|
}
|
|
@@ -781,7 +841,9 @@ export class ReminderRunner {
|
|
|
781
841
|
updatedAt: new Date().toISOString(),
|
|
782
842
|
};
|
|
783
843
|
await this.persist(updated);
|
|
784
|
-
this.
|
|
844
|
+
if (this.platformTimers === undefined) {
|
|
845
|
+
this.armTimer(updated);
|
|
846
|
+
}
|
|
785
847
|
return;
|
|
786
848
|
}
|
|
787
849
|
|
|
@@ -817,7 +879,9 @@ export class ReminderRunner {
|
|
|
817
879
|
updatedAt: new Date().toISOString(),
|
|
818
880
|
};
|
|
819
881
|
await this.persist(updated);
|
|
820
|
-
this.
|
|
882
|
+
if (this.platformTimers === undefined) {
|
|
883
|
+
this.armTimer(updated);
|
|
884
|
+
}
|
|
821
885
|
}
|
|
822
886
|
}
|
|
823
887
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Reminder document store used by the local control plane
|
|
3
|
+
* (`<stateRoot>/control-plane/reminders/`).
|
|
3
4
|
*
|
|
4
5
|
* Handlers (`run` / function `until`) are not serializable — they live only
|
|
5
|
-
* in the in-memory {@link ReminderRunner} registry.
|
|
6
|
-
* prompt, target, and status so
|
|
7
|
-
*
|
|
6
|
+
* in the in-memory {@link ReminderRunner} registry. The control plane holds
|
|
7
|
+
* schedule, prompt, target, and status so prompt-form reminders survive
|
|
8
|
+
* restart.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type TurnOutcome,
|
|
28
28
|
type TurnUsage,
|
|
29
29
|
} from "../types.js";
|
|
30
|
+
import { mapSdkTokenUsage, usageTokenSum } from "./cloud-turn-cost.js";
|
|
30
31
|
|
|
31
32
|
/** Internal seam between the session engine and the Cursor SDK harness. */
|
|
32
33
|
export interface AgentRunner {
|
|
@@ -215,7 +216,7 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
215
216
|
}
|
|
216
217
|
try {
|
|
217
218
|
const result = await run.wait();
|
|
218
|
-
|
|
219
|
+
let outcome = toTurnOutcome(result);
|
|
219
220
|
if (outcome.status === "error") {
|
|
220
221
|
// An error result usually means the handle's transport is poisoned
|
|
221
222
|
// — most often a cached access token that aged past its 60-minute
|
|
@@ -224,6 +225,16 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
224
225
|
// attempt resumes the conversation from the store on a fresh
|
|
225
226
|
// transport instead of failing every turn until a restart.
|
|
226
227
|
this.evictAgent(request.sessionId, agentPromise);
|
|
228
|
+
} else if (
|
|
229
|
+
request.runtime === "cloud" &&
|
|
230
|
+
outcome.status === "finished" &&
|
|
231
|
+
usageTokenSum(outcome.usage) <= 0 &&
|
|
232
|
+
result.id !== ""
|
|
233
|
+
) {
|
|
234
|
+
// Cloud `wait()` drops usage after stream reconnect / getRun replay,
|
|
235
|
+
// or returns an all-zero stub. `getUsage` still has the run's tokens
|
|
236
|
+
// (eventually); without them traces/ABs never record an estimated cost.
|
|
237
|
+
outcome = await fillMissingCloudUsage(agent, result.id, outcome);
|
|
227
238
|
}
|
|
228
239
|
return outcome;
|
|
229
240
|
} finally {
|
|
@@ -553,6 +564,43 @@ function toTurnOutcome(result: RunResult): TurnOutcome {
|
|
|
553
564
|
};
|
|
554
565
|
}
|
|
555
566
|
|
|
567
|
+
/** Billing can lag briefly after `wait()`; retry before giving up on cost. */
|
|
568
|
+
const CLOUD_USAGE_RETRY_DELAYS_MS = [0, 400, 1200] as const;
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Cloud `wait()` often omits usage after a stream drop. Pull the run's
|
|
572
|
+
* tokens from the usage API so the engine can still estimate cost.
|
|
573
|
+
* Failures are ignored: a missing estimate is better than failing the turn.
|
|
574
|
+
*/
|
|
575
|
+
async function fillMissingCloudUsage(
|
|
576
|
+
agent: SDKAgent,
|
|
577
|
+
runId: string,
|
|
578
|
+
outcome: TurnOutcome
|
|
579
|
+
): Promise<TurnOutcome> {
|
|
580
|
+
for (const delayMs of CLOUD_USAGE_RETRY_DELAYS_MS) {
|
|
581
|
+
if (delayMs > 0) {
|
|
582
|
+
await sleep(delayMs);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const billed = await agent.getUsage({ runId });
|
|
586
|
+
const mapped = mapSdkTokenUsage(billed.usage);
|
|
587
|
+
if (usageTokenSum(mapped) <= 0) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
return { ...outcome, usage: mapped };
|
|
591
|
+
} catch {
|
|
592
|
+
// Transient usage-API failures should still burn the retry budget.
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return outcome;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sleep(ms: number): Promise<void> {
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
setTimeout(resolve, ms);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
556
604
|
export function runErrorMessage(result: Pick<RunResult, "error">): string {
|
|
557
605
|
return result.error?.message?.trim() || "The Cursor agent run failed";
|
|
558
606
|
}
|
package/src/internal/server.ts
CHANGED
|
@@ -98,10 +98,19 @@ import {
|
|
|
98
98
|
requireSessionOwner,
|
|
99
99
|
} from "./http-channel.js";
|
|
100
100
|
import { validateToolInput } from "./json-schema.js";
|
|
101
|
+
import {
|
|
102
|
+
createLocalControlPlane,
|
|
103
|
+
type LocalControlPlane,
|
|
104
|
+
} from "./local-control-plane.js";
|
|
101
105
|
import { createLogRing, type LogRing } from "./log-ring.js";
|
|
102
106
|
import { createAgentMcpEndpoint, MCP_ENDPOINT_PATH } from "./mcp-endpoint.js";
|
|
103
107
|
import { isLoopbackAddress } from "./net.js";
|
|
104
108
|
import { collectPeerConnections } from "./peer-connections.js";
|
|
109
|
+
import {
|
|
110
|
+
PLATFORM_SCHEDULE_CATALOG_FILENAME,
|
|
111
|
+
syncHostedPlatformSchedules,
|
|
112
|
+
} from "./platform-schedule-sync.js";
|
|
113
|
+
import { createCursorHostedPlatformTimers } from "./platform-timers.js";
|
|
105
114
|
import {
|
|
106
115
|
type AgentIndexEntry,
|
|
107
116
|
renderAgentsIndexHtml,
|
|
@@ -155,6 +164,7 @@ interface MountedRuntime {
|
|
|
155
164
|
engine: SessionEngine;
|
|
156
165
|
scheduleRunner: ScheduleRunner;
|
|
157
166
|
reminderRunner: ReminderRunner;
|
|
167
|
+
controlPlane?: LocalControlPlane;
|
|
158
168
|
/** Playground eval batches (`/v1/dev/evals`); loopbackUrl set after listen. */
|
|
159
169
|
evalRuns: EvalRunStore;
|
|
160
170
|
}
|
|
@@ -196,6 +206,7 @@ export async function startServer(
|
|
|
196
206
|
);
|
|
197
207
|
const dev = options.dev ?? false;
|
|
198
208
|
const playground = options.playground ?? true;
|
|
209
|
+
const noControlPlane = options.noControlPlane === true;
|
|
199
210
|
const runSchedules = options.schedules ?? !dev;
|
|
200
211
|
const runReminders = options.reminders ?? !dev;
|
|
201
212
|
|
|
@@ -408,7 +419,21 @@ export async function startServer(
|
|
|
408
419
|
githubAuthWarmed = true;
|
|
409
420
|
}
|
|
410
421
|
const scheduleRunner = new ScheduleRunner(engine, project, logger);
|
|
411
|
-
|
|
422
|
+
let reminderRunner!: ReminderRunner;
|
|
423
|
+
const localControlPlane = noControlPlane
|
|
424
|
+
? undefined
|
|
425
|
+
: await createLocalControlPlane({
|
|
426
|
+
fireSchedule: (name) => scheduleRunner.dispatch(name),
|
|
427
|
+
fireReminder: (id, fireCount) =>
|
|
428
|
+
reminderRunner.dispatch(id, { expectedFireCount: fireCount }),
|
|
429
|
+
stateDir: join(stateRoot, "control-plane"),
|
|
430
|
+
armReminderClocks: runReminders,
|
|
431
|
+
logger,
|
|
432
|
+
});
|
|
433
|
+
const timerClient = noControlPlane
|
|
434
|
+
? createCursorHostedPlatformTimers()
|
|
435
|
+
: localControlPlane;
|
|
436
|
+
reminderRunner = new ReminderRunner(engine, logger, timerClient);
|
|
412
437
|
const evalRuns = new EvalRunStore(
|
|
413
438
|
project.rootDir,
|
|
414
439
|
logger,
|
|
@@ -443,12 +468,16 @@ export async function startServer(
|
|
|
443
468
|
scheduleRunner,
|
|
444
469
|
reminderRunner,
|
|
445
470
|
evalRuns,
|
|
471
|
+
...(localControlPlane === undefined
|
|
472
|
+
? {}
|
|
473
|
+
: { controlPlane: localControlPlane }),
|
|
446
474
|
};
|
|
447
475
|
runtimes.push(runtime);
|
|
448
476
|
const accountRefs = accountRefsBySlug.get(slug);
|
|
449
477
|
mountAgentRoutes(router, runtime, {
|
|
450
478
|
dev,
|
|
451
479
|
playground,
|
|
480
|
+
noControlPlane,
|
|
452
481
|
multi,
|
|
453
482
|
logger,
|
|
454
483
|
logRing,
|
|
@@ -509,12 +538,18 @@ export async function startServer(
|
|
|
509
538
|
}
|
|
510
539
|
}
|
|
511
540
|
}
|
|
512
|
-
if (runSchedules) {
|
|
513
|
-
|
|
541
|
+
if (timerClient !== undefined && runSchedules) {
|
|
542
|
+
await syncHostedPlatformSchedules({
|
|
543
|
+
timers: timerClient,
|
|
544
|
+
schedules: project.schedules,
|
|
545
|
+
catalogPath: join(stateRoot, PLATFORM_SCHEDULE_CATALOG_FILENAME),
|
|
546
|
+
logger,
|
|
547
|
+
});
|
|
514
548
|
}
|
|
515
|
-
if (
|
|
516
|
-
await reminderRunner.
|
|
549
|
+
if (timerClient !== undefined) {
|
|
550
|
+
await reminderRunner.hydrate();
|
|
517
551
|
}
|
|
552
|
+
localControlPlane?.startClocks();
|
|
518
553
|
}
|
|
519
554
|
|
|
520
555
|
if (docsSite) {
|
|
@@ -806,6 +841,7 @@ export async function startServer(
|
|
|
806
841
|
await cursorEventRelay?.stop();
|
|
807
842
|
await cursorSlackRelay?.stop();
|
|
808
843
|
for (const runtime of runtimes) {
|
|
844
|
+
runtime.controlPlane?.stop();
|
|
809
845
|
runtime.scheduleRunner.stop();
|
|
810
846
|
runtime.reminderRunner.stop();
|
|
811
847
|
// Before the engines close their storage coordinators, so the
|
|
@@ -849,6 +885,7 @@ function mountAgentRoutes(
|
|
|
849
885
|
args: {
|
|
850
886
|
dev: boolean;
|
|
851
887
|
playground: boolean;
|
|
888
|
+
noControlPlane?: boolean;
|
|
852
889
|
multi: boolean;
|
|
853
890
|
logger: (line: string) => void;
|
|
854
891
|
logRing: LogRing;
|
|
@@ -860,7 +897,7 @@ function mountAgentRoutes(
|
|
|
860
897
|
}
|
|
861
898
|
): void {
|
|
862
899
|
const { slug, project, engine, scheduleRunner, reminderRunner } = runtime;
|
|
863
|
-
const { dev, playground, multi, logger, logRing } = args;
|
|
900
|
+
const { dev, playground, noControlPlane, multi, logger, logRing } = args;
|
|
864
901
|
// "" (single) -> "/v1/..."; "my-agent" (multi) -> "/my-agent/v1/...".
|
|
865
902
|
const base = slug === "" ? "" : `/${slug}`;
|
|
866
903
|
|
|
@@ -893,6 +930,18 @@ function mountAgentRoutes(
|
|
|
893
930
|
? [allowAll()]
|
|
894
931
|
: [localDevStrict()];
|
|
895
932
|
const httpAuth = project.httpChannel.auth ?? defaultAuth;
|
|
933
|
+
// Temporal posts this marker on a direct pod port-token fetch. The alias
|
|
934
|
+
// proxy strips it, so webhook/playground tokens cannot present it.
|
|
935
|
+
const platformTimerFireAuth: AuthPolicy = (request) => {
|
|
936
|
+
if (request.headers.get("x-agent-serve-timed-work") === "1") {
|
|
937
|
+
return {
|
|
938
|
+
authenticator: "app",
|
|
939
|
+
principalId: "agent-serve:timed-work",
|
|
940
|
+
principalType: "runtime",
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
return null;
|
|
944
|
+
};
|
|
896
945
|
// Playground lists/opens Slack (and other channel) sessions across
|
|
897
946
|
// principals when `--dev` (loopback) or `--allow-anonymous` (trusted
|
|
898
947
|
// shared host, e.g. corp Okta ALB). Never when bearer / custom channel
|
|
@@ -1294,6 +1343,72 @@ function mountAgentRoutes(
|
|
|
1294
1343
|
},
|
|
1295
1344
|
});
|
|
1296
1345
|
|
|
1346
|
+
if (noControlPlane === true) {
|
|
1347
|
+
router.add({
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
pattern: `${base}/v1/internal/schedules/:scheduleId/fire`,
|
|
1350
|
+
auth: [platformTimerFireAuth],
|
|
1351
|
+
handler: async (_request, ctx) => {
|
|
1352
|
+
const scheduleId = ctx.params.scheduleId ?? "";
|
|
1353
|
+
try {
|
|
1354
|
+
const { sessionIds } = await scheduleRunner.dispatch(scheduleId);
|
|
1355
|
+
return jsonResponse({ scheduleId, sessionIds });
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
if (error instanceof UnknownScheduleError) {
|
|
1358
|
+
return jsonResponse(
|
|
1359
|
+
{
|
|
1360
|
+
ok: false,
|
|
1361
|
+
error: "unknown_schedule",
|
|
1362
|
+
availableScheduleIds: error.availableScheduleIds,
|
|
1363
|
+
},
|
|
1364
|
+
{ status: 404 }
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
router.add({
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
pattern: `${base}/v1/internal/reminders/:reminderId/fire`,
|
|
1374
|
+
auth: [platformTimerFireAuth],
|
|
1375
|
+
handler: async (request, ctx) => {
|
|
1376
|
+
const reminderId = ctx.params.reminderId ?? "";
|
|
1377
|
+
const fireCountRaw = new URL(request.url).searchParams.get("fireCount");
|
|
1378
|
+
const expectedFireCount =
|
|
1379
|
+
fireCountRaw === null ? undefined : Number(fireCountRaw);
|
|
1380
|
+
try {
|
|
1381
|
+
const result = await reminderRunner.dispatch(
|
|
1382
|
+
reminderId,
|
|
1383
|
+
expectedFireCount !== undefined &&
|
|
1384
|
+
Number.isInteger(expectedFireCount)
|
|
1385
|
+
? { expectedFireCount }
|
|
1386
|
+
: {}
|
|
1387
|
+
);
|
|
1388
|
+
return jsonResponse(result);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
if (error instanceof UnknownReminderError) {
|
|
1391
|
+
return jsonResponse(
|
|
1392
|
+
{
|
|
1393
|
+
ok: false,
|
|
1394
|
+
error: "unknown_reminder",
|
|
1395
|
+
availableReminderIds: error.availableReminderIds,
|
|
1396
|
+
},
|
|
1397
|
+
{ status: 404 }
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
if (error instanceof ReminderValidationError) {
|
|
1401
|
+
return jsonResponse(
|
|
1402
|
+
{ ok: false, error: "invalid_reminder", message: error.message },
|
|
1403
|
+
{ status: 400 }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
throw error;
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1297
1412
|
if (dev) {
|
|
1298
1413
|
router.add({
|
|
1299
1414
|
method: "POST",
|