@nylorun/runtime 0.36.0-beta → 0.37.0-beta
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/CHANGELOG.md +60 -0
- package/README.md +10 -0
- package/dist/api/a2a/card.d.ts +10 -4
- package/dist/api/a2a/card.js +18 -6
- package/dist/api/a2a/endpoint.d.ts +5 -2
- package/dist/api/a2a/endpoint.js +31 -9
- package/dist/api/a2a/extensions.d.ts +41 -0
- package/dist/api/a2a/extensions.js +92 -0
- package/dist/api/a2a/list.d.ts +60 -0
- package/dist/api/a2a/list.js +169 -0
- package/dist/api/a2a/protocol.d.ts +105 -9
- package/dist/api/a2a/protocol.js +169 -18
- package/dist/api/a2a/push.d.ts +76 -0
- package/dist/api/a2a/push.js +305 -0
- package/dist/api/a2a/routes.d.ts +12 -6
- package/dist/api/a2a/routes.js +409 -112
- package/dist/api/a2a/tasks.d.ts +72 -4
- package/dist/api/a2a/tasks.js +305 -27
- package/dist/api/components.d.ts +532 -0
- package/dist/api/components.js +3 -1
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/routes/artifacts.js +19 -0
- package/dist/api/http/routes/turns.d.ts +18 -0
- package/dist/api/http/routes/turns.js +67 -0
- package/dist/api/inbound-files.d.ts +12 -1
- package/dist/api/inbound-files.js +44 -0
- package/dist/api/openapi.js +7 -2
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/main.js +1 -0
- package/dist/host/stack-config.d.ts +16 -0
- package/dist/host/stack-config.js +30 -0
- package/dist/management-openapi.json +5 -2
- package/dist/openapi.json +1485 -738
- package/dist/reads/types.d.ts +19 -0
- package/dist/store/postgres/drizzle/0039_a2a_push.sql +32 -0
- package/dist/store/postgres/drizzle/0040_turn_index.sql +23 -0
- package/dist/store/postgres/drizzle/0041_turn_index_backfill.sql +88 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +21 -0
- package/dist/store/postgres/reads.js +41 -1
- package/dist/store/postgres/schema.d.ts +1296 -534
- package/dist/store/postgres/schema.js +92 -0
- package/dist/store/postgres/store.js +133 -3
- package/dist/store/postgres/turns.d.ts +62 -0
- package/dist/store/postgres/turns.js +153 -0
- package/dist/store/types.d.ts +73 -3
- package/dist/tenant/runtime.js +7 -5
- package/dist/tenant/session-model.d.ts +11 -1
- package/dist/tenant/session-model.js +34 -5
- package/dist/tenant/sessions.d.ts +2 -1
- package/dist/tenant/sessions.js +8 -2
- package/dist/tenant/sweep.d.ts +3 -0
- package/dist/tenant/sweep.js +39 -11
- package/dist/tenant/types.d.ts +23 -0
- package/dist/tuning.d.ts +9 -0
- package/dist/tuning.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -495,6 +495,46 @@ export const modelBudgets = nylorun.table("model_budgets", {
|
|
|
495
495
|
limitTokens: bigint({ mode: "number" }),
|
|
496
496
|
updatedAt: text().notNull(),
|
|
497
497
|
}, (t) => [primaryKey({ name: "model_budgets_pkey", columns: [t.scope, t.scopeId] })]);
|
|
498
|
+
/**
|
|
499
|
+
* The turn index (A2A design §9, Host feature `turns`): one row per turn, written by `Tx.event`
|
|
500
|
+
* in the transaction of each event that changes the turn (`store/postgres/turns.ts`), so it
|
|
501
|
+
* agrees with the session's log. The session's Agent, owner and A2A context are copied in when
|
|
502
|
+
* the turn starts. `txid` is the transaction that last changed the row: the change feed reads
|
|
503
|
+
* rows in its order, only below every transaction still running (`pg_snapshot_xmin`), as the
|
|
504
|
+
* model call export does. `firstSeq` and `lastSeq` bound the turn's lifecycle events in the log.
|
|
505
|
+
*/
|
|
506
|
+
export const turns = nylorun.table("turns", {
|
|
507
|
+
sessionId: textC().notNull(),
|
|
508
|
+
turnId: textC().notNull(),
|
|
509
|
+
agentId: textC().notNull(),
|
|
510
|
+
/** The session owner (`ownerUserId`); null for an application's own session. */
|
|
511
|
+
ownerUserId: textC(),
|
|
512
|
+
/** `info.a2a.contextId` of an A2A context's session. */
|
|
513
|
+
a2aContextId: textC(),
|
|
514
|
+
status: text({ enum: ["running", "paused", "completed", "failed", "cancelled"] }).notNull(),
|
|
515
|
+
/** ISO times: of the turn's first event, its end, and its last status change. */
|
|
516
|
+
startedAt: textC().notNull(),
|
|
517
|
+
endedAt: textC(),
|
|
518
|
+
changedAt: textC().notNull(),
|
|
519
|
+
/** The end facts' `totals` (`{steps, tokens, costUsd}`), once it ended with them. */
|
|
520
|
+
totals: jsonText(),
|
|
521
|
+
/**
|
|
522
|
+
* While `paused`: what its first open interaction asks for, as profile v2 names it
|
|
523
|
+
* (`question`, `approval` or `connection`); null otherwise.
|
|
524
|
+
*/
|
|
525
|
+
waitingFor: text({ enum: ["question", "approval", "connection"] }),
|
|
526
|
+
/** The queued messages it took (protocol 17): their A2A task ids name it too. */
|
|
527
|
+
queuedMessageIds: text().array().notNull().default(sql `'{}'`),
|
|
528
|
+
firstSeq: bigint({ mode: "number" }).notNull(),
|
|
529
|
+
lastSeq: bigint({ mode: "number" }).notNull(),
|
|
530
|
+
txid: xid8().notNull().default(sql `pg_current_xact_id()`),
|
|
531
|
+
}, (t) => [
|
|
532
|
+
primaryKey({ name: "turns_pkey", columns: [t.sessionId, t.turnId] }),
|
|
533
|
+
index("turns_turn").on(t.turnId),
|
|
534
|
+
index("turns_changes").on(t.txid, t.sessionId, t.turnId),
|
|
535
|
+
index("turns_owner_changes").on(t.ownerUserId, t.txid),
|
|
536
|
+
index("turns_owner_agent").on(t.ownerUserId, t.agentId, t.changedAt),
|
|
537
|
+
]);
|
|
498
538
|
/**
|
|
499
539
|
* Each session's queue (protocol 17, D76): messages that arrived while its turn ran or waited, in
|
|
500
540
|
* order (`seq`), until a turn takes them. A row holds the command as sent, the turn input its
|
|
@@ -700,6 +740,58 @@ export const sandboxSignals = nylorun.table("sandbox_signals", {
|
|
|
700
740
|
index("sandbox_signals_created_at").on(t.createdAt),
|
|
701
741
|
check("sandbox_signals_kind_check", sql `${t.kind} IN ('reconcile', 'arm')`),
|
|
702
742
|
]);
|
|
743
|
+
/**
|
|
744
|
+
* A2A push notification configs (Host feature `a2a-profile-v2`): where a task's updates go, one
|
|
745
|
+
* row per task and config id (`taskId`: the A2A id its caller knows, a queued message's own until
|
|
746
|
+
* a turn takes it; `turnId`: the turn it follows). The push translator (`api/a2a/push.ts`) reads
|
|
747
|
+
* the session's events from `nextSeq` on and writes each update as an `a2a_pushes` row in one
|
|
748
|
+
* transaction; `doneAt` is set once the task's turn ended, and the row is deleted after the
|
|
749
|
+
* Host tuning's `a2a.pushConfigRetentionMs`. `extensions` are the profile extensions the caller
|
|
750
|
+
* activated when it gave the config; `baseUrl` is the Runtime's URL as that caller reached it,
|
|
751
|
+
* for file URLs; `traceId` is `telemetry/v1`'s.
|
|
752
|
+
*/
|
|
753
|
+
export const a2aPushConfigs = nylorun.table("a2a_push_configs", {
|
|
754
|
+
sessionId: textC().notNull(),
|
|
755
|
+
taskId: textC().notNull(),
|
|
756
|
+
configId: textC().notNull(),
|
|
757
|
+
contextId: text().notNull(),
|
|
758
|
+
turnId: textC().notNull(),
|
|
759
|
+
url: text().notNull(),
|
|
760
|
+
token: text(),
|
|
761
|
+
authentication: jsonText(),
|
|
762
|
+
baseUrl: text().notNull(),
|
|
763
|
+
extensions: jsonText().notNull(),
|
|
764
|
+
traceId: text(),
|
|
765
|
+
nextSeq: bigint({ mode: "number" }).notNull(),
|
|
766
|
+
createdAt: timestamp({ withTimezone: true, mode: "date" })
|
|
767
|
+
.notNull()
|
|
768
|
+
.default(sql `clock_timestamp()`),
|
|
769
|
+
doneAt: timestamp({ withTimezone: true, mode: "date" }),
|
|
770
|
+
}, (t) => [
|
|
771
|
+
primaryKey({ name: "a2a_push_configs_pkey", columns: [t.sessionId, t.taskId, t.configId] }),
|
|
772
|
+
index("a2a_push_configs_done_at").on(t.doneAt),
|
|
773
|
+
]);
|
|
774
|
+
/**
|
|
775
|
+
* The push outbox (outbox kind `push`): one row per A2A update a push config is owed, its body
|
|
776
|
+
* the exact JSON sent (a `StreamResponse`), delivered after commit and by the Tenant sweep as the
|
|
777
|
+
* other outbox rows are (`attempts`, `retry_at`, `parked_at`). The row id is the Standard
|
|
778
|
+
* Webhooks `webhook-id`, the same on every attempt. A row whose config is gone, or that kept
|
|
779
|
+
* failing for the Host tuning's `a2a.pushMaxAgeMs` (24 hours), is deleted: the change feed
|
|
780
|
+
* covers what it would have said.
|
|
781
|
+
*/
|
|
782
|
+
export const a2aPushes = nylorun.table("a2a_pushes", {
|
|
783
|
+
id: textC().primaryKey(),
|
|
784
|
+
sessionId: textC().notNull(),
|
|
785
|
+
taskId: textC().notNull(),
|
|
786
|
+
configId: textC().notNull(),
|
|
787
|
+
body: text().notNull(),
|
|
788
|
+
createdAt: timestamp({ withTimezone: true, mode: "date" })
|
|
789
|
+
.notNull()
|
|
790
|
+
.default(sql `clock_timestamp()`),
|
|
791
|
+
attempts: integer().notNull().default(0),
|
|
792
|
+
retryAt: timestamp({ withTimezone: true, mode: "date" }),
|
|
793
|
+
parkedAt: timestamp({ withTimezone: true, mode: "date" }),
|
|
794
|
+
}, (t) => [index("a2a_pushes_created_at").on(t.createdAt)]);
|
|
703
795
|
// ---------------------------------------------------------------------------
|
|
704
796
|
// nylorun_streams: the record
|
|
705
797
|
/**
|
|
@@ -59,8 +59,9 @@ import { database, driverError } from "./db.js";
|
|
|
59
59
|
import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
|
|
60
60
|
import { hostTuning } from "../../tuning.js";
|
|
61
61
|
import { createPostgresRecordReader } from "./record.js";
|
|
62
|
+
import { indexTurn, listTurns, turnOfQueuedMessage, turnRow } from "./turns.js";
|
|
62
63
|
import { postgresAgentRecordWriter, postgresRecordWriter } from "./record-writer.js";
|
|
63
|
-
import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, effects, links, modelBudgets, modelUsage, principals, sandboxes, agentEvents, agents, agentManifests, sessionEvents, sessionLogHeads, sessions, sessionQueue, agentTriggers, triggerRuns, memories, memoryVersions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, sandboxSignals, wakes, } from "./schema.js";
|
|
64
|
+
import { artifacts, artifactVersions, artifactContent, definitionFiles, definitionFileUses, commands, effects, links, modelBudgets, modelUsage, principals, sandboxes, agentEvents, agents, agentManifests, sessionEvents, sessionLogHeads, sessions, sessionQueue, a2aPushConfigs, a2aPushes, agentTriggers, triggerRuns, memories, memoryVersions, toJson, toolCrossings, signingKeys, tenant, tenantSettings, turns, vaultAudit, vaultCredentials, vaultIdempotency, vaults, sandboxSignals, wakes, } from "./schema.js";
|
|
64
65
|
export function createPostgresSessionStore(options) {
|
|
65
66
|
return new PostgresSessionStore(options);
|
|
66
67
|
}
|
|
@@ -327,7 +328,17 @@ const SIGNING_KEY_STAMP = {
|
|
|
327
328
|
};
|
|
328
329
|
/** The audit columns a row has (`ord` only orders them). */
|
|
329
330
|
const { ord: _ord, ...AUDIT } = getTableColumns(vaultAudit);
|
|
330
|
-
const SESSION_TABLES = [
|
|
331
|
+
const SESSION_TABLES = [
|
|
332
|
+
sessions,
|
|
333
|
+
commands,
|
|
334
|
+
effects,
|
|
335
|
+
links,
|
|
336
|
+
wakes,
|
|
337
|
+
sessionQueue,
|
|
338
|
+
turns,
|
|
339
|
+
a2aPushConfigs,
|
|
340
|
+
a2aPushes,
|
|
341
|
+
];
|
|
331
342
|
/**
|
|
332
343
|
* The outbox's tables, by the kind of request each holds. Their delivery columns (`id`,
|
|
333
344
|
* `created_at`, `attempts`, `retry_at`, `parked_at`) are the same.
|
|
@@ -335,7 +346,12 @@ const SESSION_TABLES = [sessions, commands, effects, links, wakes, sessionQueue]
|
|
|
335
346
|
const OUTBOX = {
|
|
336
347
|
wake: wakes,
|
|
337
348
|
sandbox: sandboxSignals,
|
|
349
|
+
push: a2aPushes,
|
|
338
350
|
};
|
|
351
|
+
/** A push outbox row as it is delivered. */
|
|
352
|
+
function outboxPush(row) {
|
|
353
|
+
return { id: row.id, sessionId: row.sessionId, taskId: row.taskId, configId: row.configId, body: row.body };
|
|
354
|
+
}
|
|
339
355
|
/** A wake outbox row as it is delivered: its own dedupe key, or one made from the row id. */
|
|
340
356
|
function outboxWake(id, reason, dedupeKey) {
|
|
341
357
|
return { reason, dedupeKey: dedupeKey ?? `wake:${id}` };
|
|
@@ -439,10 +455,25 @@ class PostgresTx {
|
|
|
439
455
|
...(writer ? { writer } : {}),
|
|
440
456
|
maxEntryBytes: await this.maxEntryBytes(),
|
|
441
457
|
});
|
|
458
|
+
// The turn index changes with the log, in this transaction (Host feature `turns`).
|
|
459
|
+
await indexTurn(db, event);
|
|
442
460
|
this.events.push(event);
|
|
443
461
|
this.generations.push(generation);
|
|
444
462
|
return structuredClone(event);
|
|
445
463
|
}
|
|
464
|
+
// --- turn index ----------------------------------------------------------
|
|
465
|
+
async turn(sessionId, turnId) {
|
|
466
|
+
this.check();
|
|
467
|
+
return turnRow(this.db, sessionId, turnId);
|
|
468
|
+
}
|
|
469
|
+
async turnOfQueuedMessage(sessionId, queuedMessageId) {
|
|
470
|
+
this.check();
|
|
471
|
+
return turnOfQueuedMessage(this.db, sessionId, queuedMessageId);
|
|
472
|
+
}
|
|
473
|
+
async listTurns(query) {
|
|
474
|
+
this.check();
|
|
475
|
+
return listTurns(this.db, query);
|
|
476
|
+
}
|
|
446
477
|
// --- wake outbox ---------------------------------------------------------
|
|
447
478
|
async wake(sessionId, wake) {
|
|
448
479
|
this.check();
|
|
@@ -514,12 +545,27 @@ class PostgresTx {
|
|
|
514
545
|
due: row.dueSeconds,
|
|
515
546
|
})),
|
|
516
547
|
];
|
|
548
|
+
const push = DUE(OUTBOX.push, graceMs);
|
|
549
|
+
const pushRows = await this.db
|
|
550
|
+
.select({ ...getTableColumns(a2aPushes), dueSeconds: push.dueSeconds })
|
|
551
|
+
.from(a2aPushes)
|
|
552
|
+
.where(and(isNull(a2aPushes.parkedAt), lte(push.due, now)))
|
|
553
|
+
.orderBy(push.due, a2aPushes.id)
|
|
554
|
+
.limit(limit);
|
|
555
|
+
rows.push(...pushRows.map((row) => ({
|
|
556
|
+
kind: "push",
|
|
557
|
+
id: row.id,
|
|
558
|
+
request: { kind: "push", push: outboxPush(row) },
|
|
559
|
+
createdAt: row.createdAt.toISOString(),
|
|
560
|
+
attempts: row.attempts,
|
|
561
|
+
due: row.dueSeconds,
|
|
562
|
+
})));
|
|
517
563
|
rows.sort((a, b) => a.due - b.due || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
518
564
|
return rows.slice(0, limit).map(({ due: _due, ...row }) => row);
|
|
519
565
|
}
|
|
520
566
|
async deleteOutbox(rows) {
|
|
521
567
|
this.check();
|
|
522
|
-
for (const kind of ["wake", "sandbox"]) {
|
|
568
|
+
for (const kind of ["wake", "sandbox", "push"]) {
|
|
523
569
|
const ids = rows.filter((row) => row.kind === kind).map((row) => row.id);
|
|
524
570
|
if (ids.length > 0)
|
|
525
571
|
await this.db.delete(OUTBOX[kind]).where(inArray(OUTBOX[kind].id, ids));
|
|
@@ -538,6 +584,90 @@ class PostgresTx {
|
|
|
538
584
|
})
|
|
539
585
|
.where(eq(table.id, row.id));
|
|
540
586
|
}
|
|
587
|
+
// --- A2A push (Host feature `a2a-profile-v2`) ---------------------------------
|
|
588
|
+
async putPushConfig(row) {
|
|
589
|
+
this.check();
|
|
590
|
+
const { sessionId: _s, taskId: _t, configId: _c, ...set } = row;
|
|
591
|
+
await this.db
|
|
592
|
+
.insert(a2aPushConfigs)
|
|
593
|
+
.values({ ...row, doneAt: row.doneAt ?? null })
|
|
594
|
+
.onConflictDoUpdate({
|
|
595
|
+
target: [a2aPushConfigs.sessionId, a2aPushConfigs.taskId, a2aPushConfigs.configId],
|
|
596
|
+
set: { ...set, doneAt: row.doneAt ?? null, createdAt: sql `clock_timestamp()` },
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
async pushConfig(sessionId, taskId, configId) {
|
|
600
|
+
this.check();
|
|
601
|
+
const [row] = await this.db
|
|
602
|
+
.select()
|
|
603
|
+
.from(a2aPushConfigs)
|
|
604
|
+
.where(and(eq(a2aPushConfigs.sessionId, sessionId), eq(a2aPushConfigs.taskId, taskId), eq(a2aPushConfigs.configId, configId)));
|
|
605
|
+
return row;
|
|
606
|
+
}
|
|
607
|
+
async pushConfigs(sessionId, taskId, options = {}) {
|
|
608
|
+
this.check();
|
|
609
|
+
const query = this.db
|
|
610
|
+
.select()
|
|
611
|
+
.from(a2aPushConfigs)
|
|
612
|
+
.where(and(eq(a2aPushConfigs.sessionId, sessionId), eq(a2aPushConfigs.taskId, taskId), options.after === undefined ? undefined : gt(a2aPushConfigs.configId, options.after)))
|
|
613
|
+
.orderBy(a2aPushConfigs.configId)
|
|
614
|
+
.$dynamic();
|
|
615
|
+
return options.limit === undefined ? query : query.limit(options.limit);
|
|
616
|
+
}
|
|
617
|
+
async deletePushConfig(sessionId, taskId, configId) {
|
|
618
|
+
this.check();
|
|
619
|
+
const deleted = await this.db
|
|
620
|
+
.delete(a2aPushConfigs)
|
|
621
|
+
.where(and(eq(a2aPushConfigs.sessionId, sessionId), eq(a2aPushConfigs.taskId, taskId), eq(a2aPushConfigs.configId, configId)))
|
|
622
|
+
.returning({ id: a2aPushConfigs.configId });
|
|
623
|
+
return deleted.length > 0;
|
|
624
|
+
}
|
|
625
|
+
async activePushConfigs(sessionId, options = {}) {
|
|
626
|
+
this.check();
|
|
627
|
+
const query = this.db
|
|
628
|
+
.select()
|
|
629
|
+
.from(a2aPushConfigs)
|
|
630
|
+
.where(and(eq(a2aPushConfigs.sessionId, sessionId), isNull(a2aPushConfigs.doneAt)))
|
|
631
|
+
.orderBy(a2aPushConfigs.taskId, a2aPushConfigs.configId)
|
|
632
|
+
.$dynamic();
|
|
633
|
+
return options.lock ? query.for("update") : query;
|
|
634
|
+
}
|
|
635
|
+
async pushSessions(limit, after) {
|
|
636
|
+
this.check();
|
|
637
|
+
const rows = await this.db
|
|
638
|
+
.selectDistinct({ sessionId: a2aPushConfigs.sessionId })
|
|
639
|
+
.from(a2aPushConfigs)
|
|
640
|
+
.where(and(isNull(a2aPushConfigs.doneAt), after === undefined ? undefined : gt(a2aPushConfigs.sessionId, after)))
|
|
641
|
+
.orderBy(a2aPushConfigs.sessionId)
|
|
642
|
+
.limit(limit);
|
|
643
|
+
return rows.map((row) => row.sessionId);
|
|
644
|
+
}
|
|
645
|
+
async advancePushConfig(key, change) {
|
|
646
|
+
this.check();
|
|
647
|
+
await this.db
|
|
648
|
+
.update(a2aPushConfigs)
|
|
649
|
+
.set({
|
|
650
|
+
turnId: change.turnId,
|
|
651
|
+
nextSeq: change.nextSeq,
|
|
652
|
+
traceId: change.traceId,
|
|
653
|
+
...(change.done ? { doneAt: sql `clock_timestamp()` } : {}),
|
|
654
|
+
})
|
|
655
|
+
.where(and(eq(a2aPushConfigs.sessionId, key.sessionId), eq(a2aPushConfigs.taskId, key.taskId), eq(a2aPushConfigs.configId, key.configId)));
|
|
656
|
+
}
|
|
657
|
+
async prunePushConfigs(before) {
|
|
658
|
+
this.check();
|
|
659
|
+
const deleted = await this.db
|
|
660
|
+
.delete(a2aPushConfigs)
|
|
661
|
+
.where(lt(a2aPushConfigs.doneAt, before))
|
|
662
|
+
.returning({ id: a2aPushConfigs.configId });
|
|
663
|
+
return deleted.length;
|
|
664
|
+
}
|
|
665
|
+
async enqueuePush(push) {
|
|
666
|
+
this.check();
|
|
667
|
+
const id = `push_${randomUUID()}`;
|
|
668
|
+
await this.db.insert(a2aPushes).values({ id, ...push });
|
|
669
|
+
this.outbox.push({ kind: "push", id, request: { kind: "push", push: { id, ...push } } });
|
|
670
|
+
}
|
|
541
671
|
// --- ownership -----------------------------------------------------------
|
|
542
672
|
async takeOwnership(sessionId, claim) {
|
|
543
673
|
this.check();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EventType, TurnStatus } from "@nylorun/core/contracts";
|
|
2
|
+
import type { Queryable } from "./db.js";
|
|
3
|
+
import { type TurnRow } from "./schema.js";
|
|
4
|
+
/** The events that change a turn's row, and the status each leaves it in. */
|
|
5
|
+
export declare const TURN_EVENT_STATUS: Readonly<Partial<Record<EventType, TurnStatus>>>;
|
|
6
|
+
/** The statuses a turn ends in. */
|
|
7
|
+
export declare const ENDED_TURN_STATUSES: ReadonlySet<TurnStatus>;
|
|
8
|
+
/** What `indexTurn` reads of an event: the envelope fields, as `appendEvent` built them. */
|
|
9
|
+
export interface IndexedEvent {
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
readonly turnId: string | null;
|
|
12
|
+
readonly seq: number;
|
|
13
|
+
readonly time: string;
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly payload: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** What an interaction asks for, as profile v2 names it (`api/a2a/tasks.ts` `interactionOf`). */
|
|
18
|
+
export type WaitingFor = "question" | "approval" | "connection";
|
|
19
|
+
/** What a `turn.paused` payload's first open interaction asks for, or null. */
|
|
20
|
+
export declare function waitingForOf(payload: unknown): WaitingFor | null;
|
|
21
|
+
/**
|
|
22
|
+
* Writes the change `event` makes to its turn's row, in the event's transaction (the session
|
|
23
|
+
* row is locked). An event without a turn, or of a type that changes none, writes nothing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function indexTurn(db: Queryable, event: IndexedEvent): Promise<void>;
|
|
26
|
+
/** One turn's row, or undefined when the session never had that turn. */
|
|
27
|
+
export declare function turnRow(db: Queryable, sessionId: string, turnId: string): Promise<TurnRow | undefined>;
|
|
28
|
+
/** The turn that took queued message `queuedMessageId` of a session, if one did. */
|
|
29
|
+
export declare function turnOfQueuedMessage(db: Queryable, sessionId: string, queuedMessageId: string): Promise<TurnRow | undefined>;
|
|
30
|
+
/** A status a listed turn may have: `paused` narrowed by what it waits for. */
|
|
31
|
+
export interface TurnState {
|
|
32
|
+
readonly status: TurnStatus;
|
|
33
|
+
readonly waitingFor?: readonly WaitingFor[];
|
|
34
|
+
}
|
|
35
|
+
/** Which turns `listTurns` reads: one owner's A2A tasks with one Agent (`ListTasks`). */
|
|
36
|
+
export interface TurnListQuery {
|
|
37
|
+
readonly ownerUserId: string;
|
|
38
|
+
readonly agentId: string;
|
|
39
|
+
/** Only turns of A2A contexts. */
|
|
40
|
+
readonly a2a: true;
|
|
41
|
+
/** Only this session (one A2A context's). */
|
|
42
|
+
readonly sessionId?: string;
|
|
43
|
+
/** Only turns in one of these states; empty matches none. */
|
|
44
|
+
readonly states?: readonly TurnState[];
|
|
45
|
+
/** Only turns whose status changed at or after this ISO time. */
|
|
46
|
+
readonly changedSince?: string;
|
|
47
|
+
/** Only turns after this one in the order: the last row of the previous page. */
|
|
48
|
+
readonly after?: {
|
|
49
|
+
readonly changedAt: string;
|
|
50
|
+
readonly sessionId: string;
|
|
51
|
+
readonly turnId: string;
|
|
52
|
+
};
|
|
53
|
+
readonly limit: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The turns `query` names, latest status change first (then by session and turn id, descending),
|
|
57
|
+
* at most `limit`, and how many match without `after` and `limit`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function listTurns(db: Queryable, query: TurnListQuery): Promise<{
|
|
60
|
+
turns: TurnRow[];
|
|
61
|
+
total: number;
|
|
62
|
+
}>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn index (A2A design §9, Host feature `turns`): the `turns` table's writes and the typed
|
|
3
|
+
* queries on it. `Tx.event` calls `indexTurn` for every event it writes, in the same
|
|
4
|
+
* transaction, so a turn's row changes exactly when its log does:
|
|
5
|
+
*
|
|
6
|
+
* - `command.message` starts the turn (`running`), copying in its session's Agent, owner and A2A
|
|
7
|
+
* context (`info.a2a.contextId`, `api/a2a/routes.ts`) and the queued message it took, if any;
|
|
8
|
+
* a further message of the same turn only adds its queued message;
|
|
9
|
+
* - `command.approve` and `command.respond` set it `running` again, `turn.paused` `paused` with
|
|
10
|
+
* what its first open interaction asks for (`waitingFor`, profile v2's kinds);
|
|
11
|
+
* - `turn.completed`, `turn.failed` and `turn.cancelled` end it, with their end facts' `totals`.
|
|
12
|
+
*
|
|
13
|
+
* Each change stamps the row with its transaction (`txid`), which orders the change feed
|
|
14
|
+
* (`reads.ts`). Rows for the turns recorded before the index are backfilled by migration
|
|
15
|
+
* `0041_turn_index_backfill`.
|
|
16
|
+
*/
|
|
17
|
+
import { and, desc, eq, isNotNull, gte, inArray, or, sql } from "drizzle-orm";
|
|
18
|
+
import { sessions, toJson, turns } from "./schema.js";
|
|
19
|
+
/** The events that change a turn's row, and the status each leaves it in. */
|
|
20
|
+
export const TURN_EVENT_STATUS = {
|
|
21
|
+
"command.message": "running",
|
|
22
|
+
"command.approve": "running",
|
|
23
|
+
"command.respond": "running",
|
|
24
|
+
"turn.paused": "paused",
|
|
25
|
+
"turn.completed": "completed",
|
|
26
|
+
"turn.failed": "failed",
|
|
27
|
+
"turn.cancelled": "cancelled",
|
|
28
|
+
};
|
|
29
|
+
/** The statuses a turn ends in. */
|
|
30
|
+
export const ENDED_TURN_STATUSES = new Set([
|
|
31
|
+
"completed",
|
|
32
|
+
"failed",
|
|
33
|
+
"cancelled",
|
|
34
|
+
]);
|
|
35
|
+
/** An ended turn's `totals`, from its end facts, or null when it has none. */
|
|
36
|
+
function totalsOf(payload) {
|
|
37
|
+
const totals = payload?.totals;
|
|
38
|
+
if (typeof totals?.steps !== "number" ||
|
|
39
|
+
typeof totals.tokens !== "number" ||
|
|
40
|
+
typeof totals.costUsd !== "number")
|
|
41
|
+
return null;
|
|
42
|
+
return { steps: totals.steps, tokens: totals.tokens, costUsd: totals.costUsd };
|
|
43
|
+
}
|
|
44
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
/** What a `turn.paused` payload's first open interaction asks for, or null. */
|
|
46
|
+
export function waitingForOf(payload) {
|
|
47
|
+
const entries = isRecord(payload) && Array.isArray(payload.interactions) ? payload.interactions : [];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!isRecord(entry) || (entry.status !== undefined && entry.status !== "interaction"))
|
|
50
|
+
continue;
|
|
51
|
+
const interaction = entry.interaction;
|
|
52
|
+
if (!isRecord(interaction) || typeof interaction.id !== "string")
|
|
53
|
+
continue;
|
|
54
|
+
const metadata = isRecord(interaction.metadata) ? interaction.metadata : undefined;
|
|
55
|
+
if (interaction.kind === "connection" || metadata?.kind === "connection")
|
|
56
|
+
return "connection";
|
|
57
|
+
return interaction.kind === "approval" ? "approval" : "question";
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Writes the change `event` makes to its turn's row, in the event's transaction (the session
|
|
63
|
+
* row is locked). An event without a turn, or of a type that changes none, writes nothing.
|
|
64
|
+
*/
|
|
65
|
+
export async function indexTurn(db, event) {
|
|
66
|
+
const status = TURN_EVENT_STATUS[event.type];
|
|
67
|
+
if (status === undefined || event.turnId === null)
|
|
68
|
+
return;
|
|
69
|
+
const ended = ENDED_TURN_STATUSES.has(status);
|
|
70
|
+
const totals = ended ? totalsOf(event.payload) : null;
|
|
71
|
+
const waitingFor = event.type === "turn.paused" ? waitingForOf(event.payload) : null;
|
|
72
|
+
const queuedMessageId = event.type === "command.message" && isRecord(event.payload) && typeof event.payload.queuedMessageId === "string"
|
|
73
|
+
? event.payload.queuedMessageId
|
|
74
|
+
: undefined;
|
|
75
|
+
const insert = sql `
|
|
76
|
+
INSERT INTO ${turns} (
|
|
77
|
+
"session_id", "turn_id", "agent_id", "owner_user_id", "a2a_context_id", "status",
|
|
78
|
+
"started_at", "ended_at", "changed_at", "totals", "waiting_for", "queued_message_ids",
|
|
79
|
+
"first_seq", "last_seq"
|
|
80
|
+
)
|
|
81
|
+
SELECT
|
|
82
|
+
${sessions.id}, ${event.turnId}, coalesce(${sessions.agentId}, ''), ${sessions.ownerUserId},
|
|
83
|
+
nylorun.doc(${sessions.body})->'info'->'a2a'->>'contextId', ${status},
|
|
84
|
+
${event.time}, ${ended ? event.time : null}, ${event.time},
|
|
85
|
+
${totals === null ? null : sql `${toJson(totals)}::text::json`}, ${waitingFor},
|
|
86
|
+
${queuedMessageId === undefined ? sql `'{}'::text[]` : sql `ARRAY[${queuedMessageId}]::text[]`},
|
|
87
|
+
${event.seq}, ${event.seq}
|
|
88
|
+
FROM ${sessions}
|
|
89
|
+
WHERE ${sessions.id} = ${event.sessionId}`;
|
|
90
|
+
// A turn's further messages (a queued batch) add their queued ids and change nothing else.
|
|
91
|
+
await db.execute(event.type === "command.message"
|
|
92
|
+
? sql `${insert} ON CONFLICT ("session_id", "turn_id") DO UPDATE SET
|
|
93
|
+
"queued_message_ids" = ${turns}."queued_message_ids" || excluded."queued_message_ids"`
|
|
94
|
+
: sql `${insert} ON CONFLICT ("session_id", "turn_id") DO UPDATE SET
|
|
95
|
+
"status" = excluded."status",
|
|
96
|
+
"ended_at" = excluded."ended_at",
|
|
97
|
+
"changed_at" = excluded."changed_at",
|
|
98
|
+
"totals" = excluded."totals",
|
|
99
|
+
"waiting_for" = excluded."waiting_for",
|
|
100
|
+
"last_seq" = excluded."last_seq",
|
|
101
|
+
"txid" = pg_current_xact_id()`);
|
|
102
|
+
}
|
|
103
|
+
/** One turn's row, or undefined when the session never had that turn. */
|
|
104
|
+
export async function turnRow(db, sessionId, turnId) {
|
|
105
|
+
const [row] = await db
|
|
106
|
+
.select()
|
|
107
|
+
.from(turns)
|
|
108
|
+
.where(and(eq(turns.sessionId, sessionId), eq(turns.turnId, turnId)));
|
|
109
|
+
return row;
|
|
110
|
+
}
|
|
111
|
+
/** The turn that took queued message `queuedMessageId` of a session, if one did. */
|
|
112
|
+
export async function turnOfQueuedMessage(db, sessionId, queuedMessageId) {
|
|
113
|
+
const [row] = await db
|
|
114
|
+
.select()
|
|
115
|
+
.from(turns)
|
|
116
|
+
.where(and(eq(turns.sessionId, sessionId), sql `${queuedMessageId} = ANY(${turns.queuedMessageIds})`))
|
|
117
|
+
.limit(1);
|
|
118
|
+
return row;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The turns `query` names, latest status change first (then by session and turn id, descending),
|
|
122
|
+
* at most `limit`, and how many match without `after` and `limit`.
|
|
123
|
+
*/
|
|
124
|
+
export async function listTurns(db, query) {
|
|
125
|
+
if (query.states?.length === 0)
|
|
126
|
+
return { turns: [], total: 0 };
|
|
127
|
+
const where = [
|
|
128
|
+
eq(turns.ownerUserId, query.ownerUserId),
|
|
129
|
+
eq(turns.agentId, query.agentId),
|
|
130
|
+
isNotNull(turns.a2aContextId),
|
|
131
|
+
];
|
|
132
|
+
if (query.sessionId !== undefined)
|
|
133
|
+
where.push(eq(turns.sessionId, query.sessionId));
|
|
134
|
+
if (query.states !== undefined)
|
|
135
|
+
where.push(or(...query.states.map((state) => and(eq(turns.status, state.status), state.waitingFor === undefined ? undefined : inArray(turns.waitingFor, [...state.waitingFor])))));
|
|
136
|
+
if (query.changedSince !== undefined)
|
|
137
|
+
where.push(gte(turns.changedAt, query.changedSince));
|
|
138
|
+
const after = query.after;
|
|
139
|
+
// One statement at a time: a transaction runs on one connection.
|
|
140
|
+
const rows = await db
|
|
141
|
+
.select()
|
|
142
|
+
.from(turns)
|
|
143
|
+
.where(and(...where, after
|
|
144
|
+
? sql `(${turns.changedAt}, ${turns.sessionId}, ${turns.turnId}) < (${after.changedAt}, ${after.sessionId}, ${after.turnId})`
|
|
145
|
+
: undefined))
|
|
146
|
+
.orderBy(desc(turns.changedAt), desc(turns.sessionId), desc(turns.turnId))
|
|
147
|
+
.limit(query.limit);
|
|
148
|
+
const [counted] = await db
|
|
149
|
+
.select({ total: sql `count(*)`.mapWith(Number) })
|
|
150
|
+
.from(turns)
|
|
151
|
+
.where(and(...where));
|
|
152
|
+
return { turns: rows, total: counted?.total ?? 0 };
|
|
153
|
+
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -65,7 +65,9 @@ import type { RecordReader } from "../streams/relay/types.js";
|
|
|
65
65
|
import type { SandboxSignal, Wake } from "../execution/types.js";
|
|
66
66
|
import type { EventPayload, EventType, EventWriter, LiveEvent, AgentEvent, AgentEventPayload, AgentEventType, AgentManifestV8, EnvironmentKind, EnvironmentRecipe, PodDesired, PodObserved, SessionEventOf } from "@nylorun/core/contracts";
|
|
67
67
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
68
|
-
import type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, PrincipalRow, ToolCrossingRow, QueuedMessageRow, AgentTriggerRow, TriggerRunRow, MemoryRow, MemoryVersionRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow } from "./postgres/schema.js";
|
|
68
|
+
import type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, PrincipalRow, ToolCrossingRow, QueuedMessageRow, TurnRow, A2aPushConfigRow, AgentTriggerRow, TriggerRunRow, MemoryRow, MemoryVersionRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow } from "./postgres/schema.js";
|
|
69
|
+
import type { TurnListQuery } from "./postgres/turns.js";
|
|
70
|
+
export type { TurnListQuery };
|
|
69
71
|
/** Tables holding JSON documents keyed by `id`. */
|
|
70
72
|
export type DocTable = "sessions" | "commands" | "effects" | "sandboxes" | "links";
|
|
71
73
|
export declare const DOC_TABLES: readonly DocTable[];
|
|
@@ -295,7 +297,7 @@ export type CommitListener = (commit: Commit) => void;
|
|
|
295
297
|
* `bytea` columns, never inside a JSON body), the vault's audit and idempotency records, the
|
|
296
298
|
* Tenant's signing keys, a model call in the usage ledger and a model budget.
|
|
297
299
|
*/
|
|
298
|
-
export type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, ToolCrossingRow, QueuedMessageRow, AgentTriggerRow, TriggerRunRow, MemoryRow, MemoryVersionRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, };
|
|
300
|
+
export type { ArtifactRow, ArtifactVersionRow, DefinitionFileRow, ModelBudgetRow, ModelUsageRow, ModelUsageWrite, ToolCrossingRow, QueuedMessageRow, TurnRow, A2aPushConfigRow, AgentTriggerRow, TriggerRunRow, MemoryRow, MemoryVersionRow, PrincipalRow, SigningKeyRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, };
|
|
299
301
|
/** Which rows of the usage ledger a total covers. */
|
|
300
302
|
export interface ModelUsageQuery {
|
|
301
303
|
scope: "tenant" | "manifest" | "turn";
|
|
@@ -383,10 +385,22 @@ export interface SessionStore {
|
|
|
383
385
|
export type OutboxWake = Wake & {
|
|
384
386
|
dedupeKey: string;
|
|
385
387
|
};
|
|
388
|
+
/**
|
|
389
|
+
* One A2A push as the outbox delivers it (`Tx.enqueuePush`): the config it goes to (by session,
|
|
390
|
+
* task and config id) and the exact body. `id` is its Standard Webhooks `webhook-id`.
|
|
391
|
+
*/
|
|
392
|
+
export interface OutboxPush {
|
|
393
|
+
id: string;
|
|
394
|
+
sessionId: string;
|
|
395
|
+
taskId: string;
|
|
396
|
+
configId: string;
|
|
397
|
+
body: string;
|
|
398
|
+
}
|
|
386
399
|
/**
|
|
387
400
|
* What a committed transaction asked of Durable Session Execution, as the outbox delivers it:
|
|
388
401
|
* a wake (`Tx.wake`), or a pod sandbox's reconcile or timer (`Tx.signalSandbox`; a reconcile
|
|
389
|
-
* carries its idempotency key, `signal:<row id>`).
|
|
402
|
+
* carries its idempotency key, `signal:<row id>`). Or an A2A push to a caller's URL (`push`,
|
|
403
|
+
* `Tx.enqueuePush`), which the Tenant sends itself (`api/a2a/push.ts`).
|
|
390
404
|
*/
|
|
391
405
|
export type OutboxRequest = {
|
|
392
406
|
kind: "wake";
|
|
@@ -396,6 +410,9 @@ export type OutboxRequest = {
|
|
|
396
410
|
kind: "sandbox";
|
|
397
411
|
sandboxId: string;
|
|
398
412
|
signal: SandboxSignal;
|
|
413
|
+
} | {
|
|
414
|
+
kind: "push";
|
|
415
|
+
push: OutboxPush;
|
|
399
416
|
};
|
|
400
417
|
/**
|
|
401
418
|
* Hands an outbox request to Durable Session Execution (`SessionStore.deliverTo`). Resolving
|
|
@@ -502,6 +519,22 @@ export interface Tx {
|
|
|
502
519
|
event<T extends EventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>,
|
|
503
520
|
/** Who wrote it (R4.9): absent, core, observed. */
|
|
504
521
|
writer?: EventWriter): Promise<SessionEventOf<T>>;
|
|
522
|
+
/**
|
|
523
|
+
* One turn's row of the turn index, or undefined when the session never had that turn. `event`
|
|
524
|
+
* keeps the index: each event that changes a turn (`command.message`, `command.approve`,
|
|
525
|
+
* `command.respond`, `turn.*`) writes its row in the same transaction.
|
|
526
|
+
*/
|
|
527
|
+
turn(sessionId: string, turnId: string): Promise<TurnRow | undefined>;
|
|
528
|
+
/** The turn that took queued message `queuedMessageId` of the session, if one did (protocol 17). */
|
|
529
|
+
turnOfQueuedMessage(sessionId: string, queuedMessageId: string): Promise<TurnRow | undefined>;
|
|
530
|
+
/**
|
|
531
|
+
* One owner's A2A tasks with one Agent from the turn index (`ListTasks`), latest status change
|
|
532
|
+
* first, with how many match before paging.
|
|
533
|
+
*/
|
|
534
|
+
listTurns(query: TurnListQuery): Promise<{
|
|
535
|
+
turns: TurnRow[];
|
|
536
|
+
total: number;
|
|
537
|
+
}>;
|
|
505
538
|
/**
|
|
506
539
|
* Asks for an advance of `sessionId` once this transaction commits. The request is a row
|
|
507
540
|
* of the wake outbox written in this transaction, so it commits, or rolls back, with what
|
|
@@ -539,6 +572,43 @@ export interface Tx {
|
|
|
539
572
|
retryInMs: number;
|
|
540
573
|
park: boolean;
|
|
541
574
|
}): Promise<void>;
|
|
575
|
+
/** Adds or replaces a task's push config (its `createdAt` and `doneAt` start again). */
|
|
576
|
+
putPushConfig(row: Omit<A2aPushConfigRow, "createdAt" | "doneAt"> & {
|
|
577
|
+
doneAt?: Date | null;
|
|
578
|
+
}): Promise<void>;
|
|
579
|
+
/** One push config of a task. */
|
|
580
|
+
pushConfig(sessionId: string, taskId: string, configId: string): Promise<A2aPushConfigRow | undefined>;
|
|
581
|
+
/** A task's push configs, by config id, after `after`, at most `limit`. */
|
|
582
|
+
pushConfigs(sessionId: string, taskId: string, options?: {
|
|
583
|
+
after?: string;
|
|
584
|
+
limit?: number;
|
|
585
|
+
}): Promise<A2aPushConfigRow[]>;
|
|
586
|
+
/** Deletes a task's push config; whether it was there. */
|
|
587
|
+
deletePushConfig(sessionId: string, taskId: string, configId: string): Promise<boolean>;
|
|
588
|
+
/** A session's push configs still following their task (no `doneAt`); with `lock`, locked. */
|
|
589
|
+
activePushConfigs(sessionId: string, options?: {
|
|
590
|
+
lock?: boolean;
|
|
591
|
+
}): Promise<A2aPushConfigRow[]>;
|
|
592
|
+
/** Sessions with a push config still following its task, by id, after `after`, at most `limit`. */
|
|
593
|
+
pushSessions(limit: number, after?: string): Promise<string[]>;
|
|
594
|
+
/** Moves a push config on: the turn it follows, the next event it reads, and whether it is done. */
|
|
595
|
+
advancePushConfig(key: {
|
|
596
|
+
sessionId: string;
|
|
597
|
+
taskId: string;
|
|
598
|
+
configId: string;
|
|
599
|
+
}, change: {
|
|
600
|
+
turnId: string;
|
|
601
|
+
nextSeq: number;
|
|
602
|
+
traceId: string | null;
|
|
603
|
+
done: boolean;
|
|
604
|
+
}): Promise<void>;
|
|
605
|
+
/** Deletes push configs done before `before`; how many. */
|
|
606
|
+
prunePushConfigs(before: Date): Promise<number>;
|
|
607
|
+
/**
|
|
608
|
+
* Writes an A2A push to the outbox (kind `push`), delivered after commit as a wake is, and by
|
|
609
|
+
* the sweep with backoff until it is delivered or too old.
|
|
610
|
+
*/
|
|
611
|
+
enqueuePush(push: Omit<OutboxPush, "id">): Promise<void>;
|
|
542
612
|
/**
|
|
543
613
|
* Locks the session and takes ownership when it has no owner or the owner's
|
|
544
614
|
* lease ended at or before `now`; then increments `epoch` and sets the lease
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -17,7 +17,8 @@ import { createSessionStreams, endAllStreams, } from "./session-streams.js";
|
|
|
17
17
|
import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
|
|
18
18
|
import { abortAll, abortLocal, createWorkState, drain, waitForIdle, } from "./scheduler.js";
|
|
19
19
|
import { advance } from "./advance.js";
|
|
20
|
-
import { sweep } from "./sweep.js";
|
|
20
|
+
import { outboxFields, sweep } from "./sweep.js";
|
|
21
|
+
import { deliverPush } from "../api/a2a/push.js";
|
|
21
22
|
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
22
23
|
import { authorize } from "./effects.js";
|
|
23
24
|
import { inProcessToolGate, openMcpThrough } from "../gates/tool-gate.js";
|
|
@@ -276,7 +277,10 @@ export class TenantRuntime {
|
|
|
276
277
|
},
|
|
277
278
|
send: (request) => request.kind === "wake"
|
|
278
279
|
? ctx.wake(request.sessionId, request.wake)
|
|
279
|
-
:
|
|
280
|
+
: request.kind === "push"
|
|
281
|
+
? // An A2A push goes to the caller's URL, not to the execution (`api/a2a/push.ts`).
|
|
282
|
+
deliverPush(ctx, request.push)
|
|
283
|
+
: ctx.sandboxSignal(request.sandboxId, request.signal),
|
|
280
284
|
...(hooks.execution?.stuckInvocations
|
|
281
285
|
? {
|
|
282
286
|
stuckInvocations: () => hooks.execution.stuckInvocations(config.tenantId),
|
|
@@ -296,9 +300,7 @@ export class TenantRuntime {
|
|
|
296
300
|
}
|
|
297
301
|
catch (error) {
|
|
298
302
|
config.logger.warn("outbox delivery after commit failed; the sweep sends it again", {
|
|
299
|
-
...(request
|
|
300
|
-
? { sessionId: request.sessionId, reason: request.wake.reason }
|
|
301
|
-
: { sandboxId: request.sandboxId, signal: request.signal.kind }),
|
|
303
|
+
...outboxFields(request),
|
|
302
304
|
message: error instanceof Error ? error.message : String(error),
|
|
303
305
|
});
|
|
304
306
|
return false;
|