@nylorun/runtime 0.36.1-beta → 0.38.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 +70 -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 +538 -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/harness/image-engine.d.ts +14 -0
- package/dist/harness/image-engine.js +16 -0
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/pod.js +5 -0
- package/dist/harness-api/ws-server.js +7 -1
- 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 +37 -2
- package/dist/openapi.json +1485 -738
- package/dist/reads/types.d.ts +19 -0
- package/dist/sandbox/join.d.ts +7 -1
- package/dist/sandbox/join.js +5 -1
- package/dist/sandbox/pods/builtin.d.ts +18 -0
- package/dist/sandbox/pods/builtin.js +33 -0
- package/dist/sandbox/pods/client.d.ts +5 -4
- package/dist/sandbox/pods/spec.js +14 -1
- 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/0042_worker_engine_image.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/reads.js +41 -1
- package/dist/store/postgres/schema.d.ts +1314 -533
- package/dist/store/postgres/schema.js +94 -0
- package/dist/store/postgres/store.js +135 -3
- package/dist/store/postgres/turns.d.ts +62 -0
- package/dist/store/postgres/turns.js +153 -0
- package/dist/store/types.d.ts +82 -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
package/dist/reads/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ArtifactKind, ArtifactPage, ModelCallExportPage, ModelCallsPage, ToolCallsPage, SessionManifestView, SessionPage, SessionListItem, SessionUsageTotals, LiveEvent } from "@nylorun/core/contracts";
|
|
2
|
+
import type { TurnRow } from "../store/types.js";
|
|
2
3
|
export type ReadAccess = {
|
|
3
4
|
owner?: string;
|
|
4
5
|
agents?: readonly string[];
|
|
@@ -14,6 +15,12 @@ export type PageOptions = {
|
|
|
14
15
|
limit: number;
|
|
15
16
|
cursor?: string;
|
|
16
17
|
};
|
|
18
|
+
/** A page of the turn change feed: rows in the order they changed, and where to read on. */
|
|
19
|
+
export type TurnChangeRows = {
|
|
20
|
+
turns: TurnRow[];
|
|
21
|
+
cursor: string;
|
|
22
|
+
hasMore: boolean;
|
|
23
|
+
};
|
|
17
24
|
export type ArtifactFilters = {
|
|
18
25
|
sessionId?: string;
|
|
19
26
|
kind?: ArtifactKind;
|
|
@@ -34,5 +41,17 @@ export interface ReadStore {
|
|
|
34
41
|
* for a turn the session never had.
|
|
35
42
|
*/
|
|
36
43
|
turnEvents(id: string, turnId: string, access: ReadAccess): Promise<LiveEvent[]>;
|
|
44
|
+
/**
|
|
45
|
+
* The turn index's change feed (Host feature `turns`): the turns `access` reaches (of one
|
|
46
|
+
* session with `sessionId`), in the order their transactions changed them, after `changedAfter`
|
|
47
|
+
* (a cursor it returned). Only changes below every transaction still running are read, so a
|
|
48
|
+
* change committed later never lands behind the cursor.
|
|
49
|
+
*/
|
|
50
|
+
turnChanges(filters: {
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
}, page: {
|
|
53
|
+
limit: number;
|
|
54
|
+
changedAfter?: string;
|
|
55
|
+
}, access: ReadAccess): Promise<TurnChangeRows>;
|
|
37
56
|
close(): Promise<void>;
|
|
38
57
|
}
|
package/dist/sandbox/join.d.ts
CHANGED
|
@@ -20,7 +20,13 @@ export declare class HostAuthError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
/** The Tenant's side of the exchange, as the Harness API listener calls it. */
|
|
22
22
|
export interface HostAuthority {
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* `imageEngine`: the Runtime version an engine the workload image carries reported (ADR-99);
|
|
25
|
+
* absent for an engine copied in.
|
|
26
|
+
*/
|
|
27
|
+
join(request: HostJoinRequest, from?: {
|
|
28
|
+
imageEngine?: string;
|
|
29
|
+
}): Promise<HostJoinAnswer>;
|
|
24
30
|
renew(hostToken: string): Promise<HostJoinAnswer>;
|
|
25
31
|
/** The claims of a current host's token, for a connection; refuses a stale one. */
|
|
26
32
|
verify(hostToken: string): Promise<HostClaims>;
|
package/dist/sandbox/join.js
CHANGED
|
@@ -85,7 +85,7 @@ export function hostAuthority(ctx) {
|
|
|
85
85
|
return verdict.claims;
|
|
86
86
|
};
|
|
87
87
|
return {
|
|
88
|
-
async join(request) {
|
|
88
|
+
async join(request, from) {
|
|
89
89
|
const { sandboxId, podUid, joinToken, volumeId } = request;
|
|
90
90
|
if (!isAgentId(sandboxId) ||
|
|
91
91
|
typeof podUid !== "string" ||
|
|
@@ -146,6 +146,10 @@ export function hostAuthority(ctx) {
|
|
|
146
146
|
hostEpoch: epoch,
|
|
147
147
|
podUid,
|
|
148
148
|
...(pod.volumeId === undefined && volumeId !== undefined ? { volumeId } : {}),
|
|
149
|
+
// The engine the image carries, against the digest this pod runs (ADR-99).
|
|
150
|
+
...(from?.imageEngine === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { engineImage: { version: from.imageEngine, ...(pod.imageDigest ? { digest: pod.imageDigest } : {}) } }),
|
|
149
153
|
...(pod.desired === "running" ? { observed: "running", reason: null } : {}),
|
|
150
154
|
}, now);
|
|
151
155
|
if (pod.podUid !== podUid) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { BUILTIN_ENGINE_ROOT, ENGINE_VERSION_PATTERN, IMAGE_ENGINE_HEADER } from "../../harness/image-engine.js";
|
|
2
|
+
/** The engine an image carries, as the pod's record keeps it: its version, seen on `digest`. */
|
|
3
|
+
export interface EngineImage {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly digest?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Whether a pod of `image` (its pinned digest, or as asked) runs the engine the image carries:
|
|
9
|
+
* the Tenant lists its repository, and the engine recorded for that digest is this Runtime's
|
|
10
|
+
* version or not known yet.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runsBuiltinEngine(input: {
|
|
13
|
+
readonly image: string | undefined;
|
|
14
|
+
readonly imageDigest: string | undefined;
|
|
15
|
+
readonly engineImage: EngineImage | undefined;
|
|
16
|
+
readonly listed: readonly string[] | undefined;
|
|
17
|
+
readonly runtimeVersion?: string;
|
|
18
|
+
}): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An engine built into the workload's image (ADR-99): the Runtime image's tini, node and bundled
|
|
3
|
+
* engine under `/opt/nylorun`, which the sandboxes service runs with `entrypoint: "builtin"` and
|
|
4
|
+
* nothing copied in.
|
|
5
|
+
*
|
|
6
|
+
* The Tenant lists the images that carry one (`engine.images`, by repository). Such an engine is
|
|
7
|
+
* whichever Runtime version the image was built from, so it reports that version when it joins
|
|
8
|
+
* (`IMAGE_ENGINE_HEADER`, sent only by an engine running from `/opt/nylorun`), and the pod's
|
|
9
|
+
* record keeps it beside the digest it ran (`engineImage`). A pod runs the image's engine while
|
|
10
|
+
* that version is the Runtime's own, or not known yet for the digest it runs (a first start, which
|
|
11
|
+
* the Harness API version check at `hello` still guards); otherwise the engine is copied in, so a
|
|
12
|
+
* Runtime upgrade never leaves a pod on an older engine for more than that first turn.
|
|
13
|
+
*/
|
|
14
|
+
import { RUNTIME_VERSION } from "../../version.js";
|
|
15
|
+
import { repositoryOf } from "./lifecycle.js";
|
|
16
|
+
export { BUILTIN_ENGINE_ROOT, ENGINE_VERSION_PATTERN, IMAGE_ENGINE_HEADER } from "../../harness/image-engine.js";
|
|
17
|
+
/**
|
|
18
|
+
* Whether a pod of `image` (its pinned digest, or as asked) runs the engine the image carries:
|
|
19
|
+
* the Tenant lists its repository, and the engine recorded for that digest is this Runtime's
|
|
20
|
+
* version or not known yet.
|
|
21
|
+
*/
|
|
22
|
+
export function runsBuiltinEngine(input) {
|
|
23
|
+
const image = input.imageDigest ?? input.image;
|
|
24
|
+
if (!image || !input.listed?.length)
|
|
25
|
+
return false;
|
|
26
|
+
const repository = repositoryOf(image);
|
|
27
|
+
if (!input.listed.some((entry) => repositoryOf(entry) === repository))
|
|
28
|
+
return false;
|
|
29
|
+
const known = input.engineImage !== undefined && input.imageDigest !== undefined && input.engineImage.digest === input.imageDigest
|
|
30
|
+
? input.engineImage.version
|
|
31
|
+
: undefined;
|
|
32
|
+
return known === undefined || known === (input.runtimeVersion ?? RUNTIME_VERSION);
|
|
33
|
+
}
|
|
@@ -5,11 +5,12 @@ export interface PodSpec {
|
|
|
5
5
|
/** The workload image; the service defaults it to `python:3.13-slim`. */
|
|
6
6
|
readonly image?: string;
|
|
7
7
|
/**
|
|
8
|
-
* What the workload runs: `engine` (default), our engine copied from `harnessImage
|
|
9
|
-
* `
|
|
8
|
+
* What the workload runs: `engine` (default), our engine copied from `harnessImage`;
|
|
9
|
+
* `builtin`, our engine the image carries under `/opt/nylorun` (ADR-99); or `image`, the image's
|
|
10
|
+
* own entrypoint, for a custom harness built into it (R4.4).
|
|
10
11
|
*/
|
|
11
|
-
readonly entrypoint?: "engine" | "image";
|
|
12
|
-
/** The Runtime image the engine is copied from; not with `entrypoint: "image"`. */
|
|
12
|
+
readonly entrypoint?: "engine" | "builtin" | "image";
|
|
13
|
+
/** The Runtime image the engine is copied from; not with `entrypoint: "builtin"` or `"image"`. */
|
|
13
14
|
readonly harnessImage?: string;
|
|
14
15
|
/** Replaces the engine command (diagnostics). */
|
|
15
16
|
readonly command?: readonly string[];
|
|
@@ -8,6 +8,7 @@ import { DEFAULT_HARNESS } from "@nylorun/core/contracts";
|
|
|
8
8
|
import { idleMsOf, memoryMiBOf } from "../policy.js";
|
|
9
9
|
import { podTuningEnv } from "../../tuning.js";
|
|
10
10
|
import { opIdOf } from "./lifecycle.js";
|
|
11
|
+
import { runsBuiltinEngine } from "./builtin.js";
|
|
11
12
|
/** The pod's grace period to stop when the Tenant sets none (D34). */
|
|
12
13
|
export const DEFAULT_STOP_GRACE_SECONDS = 10;
|
|
13
14
|
/** The volume's size when the sandbox asks for none. */
|
|
@@ -40,12 +41,24 @@ export function podSpecOf(input) {
|
|
|
40
41
|
// Pinned by digest once a pod ran it (Q14).
|
|
41
42
|
const image = pod.imageDigest ?? spec.image;
|
|
42
43
|
const ours = (input.harness ?? DEFAULT_HARNESS) === DEFAULT_HARNESS;
|
|
44
|
+
// Our engine, built into an image the Tenant lists, while it is this Runtime's (ADR-99).
|
|
45
|
+
const builtin = ours &&
|
|
46
|
+
runsBuiltinEngine({
|
|
47
|
+
image: spec.image,
|
|
48
|
+
imageDigest: pod.imageDigest,
|
|
49
|
+
engineImage: pod.engineImage,
|
|
50
|
+
listed: config.engine?.images,
|
|
51
|
+
});
|
|
43
52
|
return {
|
|
44
53
|
opId: opIdOf(pod),
|
|
45
54
|
mode: input.mode,
|
|
46
55
|
...(image ? { image } : {}),
|
|
47
56
|
// A custom harness is its image: the pod runs the image's entrypoint, nothing copied in.
|
|
48
|
-
...(ours
|
|
57
|
+
...(!ours
|
|
58
|
+
? { entrypoint: "image" }
|
|
59
|
+
: builtin
|
|
60
|
+
? { entrypoint: "builtin" }
|
|
61
|
+
: { harnessImage: input.harnessImage }),
|
|
49
62
|
cpus: spec.resources?.cpus ?? 1,
|
|
50
63
|
memoryMiB: memoryMiBOf(spec),
|
|
51
64
|
storageGiB: storageGiBOf(spec),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
CREATE TABLE "nylorun"."a2a_push_configs" (
|
|
2
|
+
"session_id" text COLLATE "C" NOT NULL,
|
|
3
|
+
"task_id" text COLLATE "C" NOT NULL,
|
|
4
|
+
"config_id" text COLLATE "C" NOT NULL,
|
|
5
|
+
"context_id" text NOT NULL,
|
|
6
|
+
"turn_id" text COLLATE "C" NOT NULL,
|
|
7
|
+
"url" text NOT NULL,
|
|
8
|
+
"token" text,
|
|
9
|
+
"authentication" json,
|
|
10
|
+
"base_url" text NOT NULL,
|
|
11
|
+
"extensions" json NOT NULL,
|
|
12
|
+
"trace_id" text,
|
|
13
|
+
"next_seq" bigint NOT NULL,
|
|
14
|
+
"created_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL,
|
|
15
|
+
"done_at" timestamp with time zone,
|
|
16
|
+
CONSTRAINT "a2a_push_configs_pkey" PRIMARY KEY("session_id","task_id","config_id")
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE TABLE "nylorun"."a2a_pushes" (
|
|
20
|
+
"id" text COLLATE "C" PRIMARY KEY NOT NULL,
|
|
21
|
+
"session_id" text COLLATE "C" NOT NULL,
|
|
22
|
+
"task_id" text COLLATE "C" NOT NULL,
|
|
23
|
+
"config_id" text COLLATE "C" NOT NULL,
|
|
24
|
+
"body" text NOT NULL,
|
|
25
|
+
"created_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL,
|
|
26
|
+
"attempts" integer DEFAULT 0 NOT NULL,
|
|
27
|
+
"retry_at" timestamp with time zone,
|
|
28
|
+
"parked_at" timestamp with time zone
|
|
29
|
+
);
|
|
30
|
+
--> statement-breakpoint
|
|
31
|
+
CREATE INDEX "a2a_push_configs_done_at" ON "nylorun"."a2a_push_configs" USING btree ("done_at");--> statement-breakpoint
|
|
32
|
+
CREATE INDEX "a2a_pushes_created_at" ON "nylorun"."a2a_pushes" USING btree ("created_at");
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
CREATE TABLE "nylorun"."turns" (
|
|
2
|
+
"session_id" text COLLATE "C" NOT NULL,
|
|
3
|
+
"turn_id" text COLLATE "C" NOT NULL,
|
|
4
|
+
"agent_id" text COLLATE "C" NOT NULL,
|
|
5
|
+
"owner_user_id" text COLLATE "C",
|
|
6
|
+
"a2a_context_id" text COLLATE "C",
|
|
7
|
+
"status" text NOT NULL,
|
|
8
|
+
"started_at" text COLLATE "C" NOT NULL,
|
|
9
|
+
"ended_at" text COLLATE "C",
|
|
10
|
+
"changed_at" text COLLATE "C" NOT NULL,
|
|
11
|
+
"totals" json,
|
|
12
|
+
"waiting_for" text,
|
|
13
|
+
"queued_message_ids" text[] DEFAULT '{}' NOT NULL,
|
|
14
|
+
"first_seq" bigint NOT NULL,
|
|
15
|
+
"last_seq" bigint NOT NULL,
|
|
16
|
+
"txid" "xid8" DEFAULT pg_current_xact_id() NOT NULL,
|
|
17
|
+
CONSTRAINT "turns_pkey" PRIMARY KEY("session_id","turn_id")
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE INDEX "turns_turn" ON "nylorun"."turns" USING btree ("turn_id");--> statement-breakpoint
|
|
21
|
+
CREATE INDEX "turns_changes" ON "nylorun"."turns" USING btree ("txid","session_id","turn_id");--> statement-breakpoint
|
|
22
|
+
CREATE INDEX "turns_owner_changes" ON "nylorun"."turns" USING btree ("owner_user_id","txid");--> statement-breakpoint
|
|
23
|
+
CREATE INDEX "turns_owner_agent" ON "nylorun"."turns" USING btree ("owner_user_id","agent_id","changed_at");
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
-- Custom SQL (a data change): the turn index (0040) for the turns the record already holds. Each
|
|
2
|
+
-- turn's row is what `Tx.event` would have written for its lifecycle events (`command.message`,
|
|
3
|
+
-- `command.approve`, `command.respond`, `turn.paused`, `turn.completed`, `turn.failed`,
|
|
4
|
+
-- `turn.cancelled`): its status from the last of them (and, paused, what its first open
|
|
5
|
+
-- interaction asks for), its start from the first, and the queued messages it took. Its session's
|
|
6
|
+
-- Agent, owner and A2A context come from the session row as it is now. Rows written from here on
|
|
7
|
+
-- come from `Tx.event`, in the transaction of the event.
|
|
8
|
+
WITH lifecycle AS (
|
|
9
|
+
SELECT "session_id", "seq", "type", nylorun.doc("body") AS doc
|
|
10
|
+
FROM "nylorun_streams"."session_events"
|
|
11
|
+
WHERE "type" IN (
|
|
12
|
+
'command.message', 'command.approve', 'command.respond',
|
|
13
|
+
'turn.paused', 'turn.completed', 'turn.failed', 'turn.cancelled'
|
|
14
|
+
)
|
|
15
|
+
), turned AS (
|
|
16
|
+
SELECT "session_id", doc->>'turnId' AS turn_id, "seq", "type", doc
|
|
17
|
+
FROM lifecycle
|
|
18
|
+
WHERE doc->>'turnId' IS NOT NULL
|
|
19
|
+
), bounds AS (
|
|
20
|
+
SELECT "session_id", turn_id, min("seq") AS first_seq,
|
|
21
|
+
min(doc->>'time') AS started_at,
|
|
22
|
+
coalesce(
|
|
23
|
+
array_remove(
|
|
24
|
+
array_agg(doc->'payload'->>'queuedMessageId' ORDER BY "seq")
|
|
25
|
+
FILTER (WHERE "type" = 'command.message'),
|
|
26
|
+
NULL
|
|
27
|
+
),
|
|
28
|
+
'{}'
|
|
29
|
+
) AS queued_message_ids
|
|
30
|
+
FROM turned
|
|
31
|
+
GROUP BY "session_id", turn_id
|
|
32
|
+
), latest AS (
|
|
33
|
+
-- A turn's further messages (a queued batch) change nothing but its queued messages.
|
|
34
|
+
SELECT DISTINCT ON (t."session_id", t.turn_id) t."session_id", t.turn_id, t."seq", t."type", t.doc
|
|
35
|
+
FROM turned t
|
|
36
|
+
JOIN bounds b ON b."session_id" = t."session_id" AND b.turn_id = t.turn_id
|
|
37
|
+
WHERE NOT (t."type" = 'command.message' AND t."seq" > b.first_seq)
|
|
38
|
+
ORDER BY t."session_id", t.turn_id, t."seq" DESC
|
|
39
|
+
)
|
|
40
|
+
INSERT INTO "nylorun"."turns" (
|
|
41
|
+
"session_id", "turn_id", "agent_id", "owner_user_id", "a2a_context_id", "status",
|
|
42
|
+
"started_at", "ended_at", "changed_at", "totals", "waiting_for", "queued_message_ids",
|
|
43
|
+
"first_seq", "last_seq"
|
|
44
|
+
)
|
|
45
|
+
SELECT
|
|
46
|
+
b."session_id",
|
|
47
|
+
b.turn_id,
|
|
48
|
+
coalesce(s."agent_id", ''),
|
|
49
|
+
s."owner_user_id",
|
|
50
|
+
nylorun.doc(s."body")->'info'->'a2a'->>'contextId',
|
|
51
|
+
CASE l."type"
|
|
52
|
+
WHEN 'turn.completed' THEN 'completed'
|
|
53
|
+
WHEN 'turn.failed' THEN 'failed'
|
|
54
|
+
WHEN 'turn.cancelled' THEN 'cancelled'
|
|
55
|
+
WHEN 'turn.paused' THEN 'paused'
|
|
56
|
+
ELSE 'running'
|
|
57
|
+
END,
|
|
58
|
+
b.started_at,
|
|
59
|
+
CASE WHEN l."type" IN ('turn.completed', 'turn.failed', 'turn.cancelled') THEN l.doc->>'time' END,
|
|
60
|
+
l.doc->>'time',
|
|
61
|
+
CASE WHEN l."type" IN ('turn.completed', 'turn.failed', 'turn.cancelled')
|
|
62
|
+
AND jsonb_typeof(l.doc->'payload'->'totals'->'steps') = 'number'
|
|
63
|
+
AND jsonb_typeof(l.doc->'payload'->'totals'->'tokens') = 'number'
|
|
64
|
+
AND jsonb_typeof(l.doc->'payload'->'totals'->'costUsd') = 'number'
|
|
65
|
+
THEN json_build_object(
|
|
66
|
+
'steps', l.doc->'payload'->'totals'->'steps',
|
|
67
|
+
'tokens', l.doc->'payload'->'totals'->'tokens',
|
|
68
|
+
'costUsd', l.doc->'payload'->'totals'->'costUsd'
|
|
69
|
+
) END,
|
|
70
|
+
CASE WHEN l."type" = 'turn.paused' AND jsonb_typeof(l.doc->'payload'->'interactions') = 'array' THEN (
|
|
71
|
+
SELECT CASE
|
|
72
|
+
WHEN x.e->'interaction'->>'kind' = 'connection'
|
|
73
|
+
OR x.e->'interaction'->'metadata'->>'kind' = 'connection' THEN 'connection'
|
|
74
|
+
WHEN x.e->'interaction'->>'kind' = 'approval' THEN 'approval'
|
|
75
|
+
ELSE 'question'
|
|
76
|
+
END
|
|
77
|
+
FROM jsonb_array_elements(l.doc->'payload'->'interactions') WITH ORDINALITY AS x(e, i)
|
|
78
|
+
WHERE (x.e->>'status' IS NULL OR x.e->>'status' = 'interaction')
|
|
79
|
+
AND jsonb_typeof(x.e->'interaction'->'id') = 'string'
|
|
80
|
+
ORDER BY x.i
|
|
81
|
+
LIMIT 1
|
|
82
|
+
) END,
|
|
83
|
+
b.queued_message_ids,
|
|
84
|
+
b.first_seq,
|
|
85
|
+
l."seq"
|
|
86
|
+
FROM bounds b
|
|
87
|
+
JOIN latest l ON l."session_id" = b."session_id" AND l.turn_id = b.turn_id
|
|
88
|
+
LEFT JOIN "nylorun"."sessions" s ON s."id" = b."session_id";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "nylorun"."agents" ADD COLUMN "engine_image" json;
|
|
@@ -274,6 +274,34 @@
|
|
|
274
274
|
"when": 1792484344984,
|
|
275
275
|
"tag": "0038_t6_agent_manifest_source",
|
|
276
276
|
"breakpoints": true
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"idx": 39,
|
|
280
|
+
"version": "7",
|
|
281
|
+
"when": 1793445550433,
|
|
282
|
+
"tag": "0039_a2a_push",
|
|
283
|
+
"breakpoints": true
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"idx": 40,
|
|
287
|
+
"version": "7",
|
|
288
|
+
"when": 1793448351698,
|
|
289
|
+
"tag": "0040_turn_index",
|
|
290
|
+
"breakpoints": true
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"idx": 41,
|
|
294
|
+
"version": "7",
|
|
295
|
+
"when": 1793448353003,
|
|
296
|
+
"tag": "0041_turn_index_backfill",
|
|
297
|
+
"breakpoints": true
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"idx": 42,
|
|
301
|
+
"version": "7",
|
|
302
|
+
"when": 1793509192153,
|
|
303
|
+
"tag": "0042_worker_engine_image",
|
|
304
|
+
"breakpoints": true
|
|
277
305
|
}
|
|
278
306
|
]
|
|
279
307
|
}
|
|
@@ -11,7 +11,7 @@ import { database, driverError } from "./db.js";
|
|
|
11
11
|
import { createPostgresReadClient } from "./connect.js";
|
|
12
12
|
import { hostTuning } from "../../tuning.js";
|
|
13
13
|
import { toolCallDuration, toolCallStatus } from "../../harness-api/tool-status.js";
|
|
14
|
-
import { effects, artifacts, modelUsage, sandboxes, sessions, sessionEvents, tenantSettings, } from "./schema.js";
|
|
14
|
+
import { effects, artifacts, modelUsage, sandboxes, sessions, sessionEvents, tenantSettings, turns, } from "./schema.js";
|
|
15
15
|
const sessionFields = {
|
|
16
16
|
id: sessions.id,
|
|
17
17
|
agentId: sessions.agentId,
|
|
@@ -101,6 +101,20 @@ function publicToolCall(body) {
|
|
|
101
101
|
durationMs: toolCallDuration(body) ?? null,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
/** The change feed's cursor key: the last row's `[txid, sessionId, turnId]`. */
|
|
105
|
+
function turnChangeKey(key) {
|
|
106
|
+
if (key === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
const [txid, sessionId, turnId] = key;
|
|
109
|
+
if (key.length !== 3 ||
|
|
110
|
+
typeof txid !== "string" ||
|
|
111
|
+
typeof sessionId !== "string" ||
|
|
112
|
+
typeof turnId !== "string" ||
|
|
113
|
+
!/^\d{1,20}$/.test(txid) ||
|
|
114
|
+
BigInt(txid) > 18446744073709551615n)
|
|
115
|
+
return fail(400, "Invalid cursor key", { code: "cursor_invalid" });
|
|
116
|
+
return [txid, sessionId, turnId];
|
|
117
|
+
}
|
|
104
118
|
function keyOf(key, length, timestamp = false, nullable = false) {
|
|
105
119
|
if (key &&
|
|
106
120
|
(key.length !== length ||
|
|
@@ -316,6 +330,32 @@ export function createPostgresReadStore(source, tenantId) {
|
|
|
316
330
|
.orderBy(asc(sessionEvents.seq));
|
|
317
331
|
return rows.map((row) => row.body);
|
|
318
332
|
}),
|
|
333
|
+
turnChanges: (filters, page, access) => read(async (tx) => {
|
|
334
|
+
if (filters.sessionId !== undefined)
|
|
335
|
+
await requireSession(tx, filters.sessionId, access);
|
|
336
|
+
const cursor = readCursor(tenantId, "turns", { sessionId: filters.sessionId ?? null });
|
|
337
|
+
const key = turnChangeKey(cursor.decode(page.changedAfter));
|
|
338
|
+
const rows = await tx
|
|
339
|
+
.select()
|
|
340
|
+
.from(turns)
|
|
341
|
+
.where(and(
|
|
342
|
+
// Below every transaction still running: one that commits later sorts after.
|
|
343
|
+
sql `${turns.txid} < pg_snapshot_xmin(pg_current_snapshot())`, key
|
|
344
|
+
? sql `(${turns.txid}, ${turns.sessionId}, ${turns.turnId}) > (${key[0]}::xid8, ${key[1]}, ${key[2]})`
|
|
345
|
+
: undefined, filters.sessionId === undefined ? undefined : eq(turns.sessionId, filters.sessionId), access.owner === undefined ? undefined : eq(turns.ownerUserId, access.owner), access.agents === undefined ? undefined : grantsWhere(turns.agentId, access.agents)))
|
|
346
|
+
.orderBy(asc(turns.txid), asc(turns.sessionId), asc(turns.turnId))
|
|
347
|
+
.limit(page.limit + 1);
|
|
348
|
+
const items = rows.slice(0, page.limit);
|
|
349
|
+
const last = items.at(-1);
|
|
350
|
+
return {
|
|
351
|
+
turns: items,
|
|
352
|
+
// Echoed when nothing changed; a first read starts before every row.
|
|
353
|
+
cursor: last
|
|
354
|
+
? cursor.encode([last.txid, last.sessionId, last.turnId])
|
|
355
|
+
: (page.changedAfter ?? cursor.encode(["0", "", ""])),
|
|
356
|
+
hasMore: rows.length > page.limit,
|
|
357
|
+
};
|
|
358
|
+
}),
|
|
319
359
|
exportModel: (after, limit) => read(async (tx) => {
|
|
320
360
|
const cursor = readCursor(tenantId, "model-export", {});
|
|
321
361
|
const key = keyOf(cursor.decode(after), 2);
|