@nylorun/runtime 0.27.0-beta → 0.29.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 +67 -0
- package/README.md +1 -1
- package/dist/api/a2a/card.d.ts +4 -3
- package/dist/api/a2a/card.js +3 -3
- package/dist/api/a2a/routes.js +18 -7
- package/dist/api/ag-ui/routes.js +4 -7
- package/dist/api/components.d.ts +5854 -2718
- package/dist/api/components.js +39 -30
- package/dist/api/http/app.js +9 -4
- package/dist/api/http/body.d.ts +2 -0
- package/dist/api/http/body.js +20 -0
- package/dist/api/http/define.d.ts +4 -0
- package/dist/api/http/define.js +20 -9
- package/dist/api/http/routes/agent-manifests.d.ts +10 -0
- package/dist/api/http/routes/agent-manifests.js +135 -0
- package/dist/api/http/routes/agents.d.ts +11 -0
- package/dist/api/http/routes/agents.js +418 -0
- package/dist/api/http/routes/artifacts.js +2 -2
- package/dist/api/http/routes/coverage.d.ts +4 -4
- package/dist/api/http/routes/coverage.js +12 -6
- package/dist/api/http/routes/files.js +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.d.ts +1 -1
- package/dist/api/http/routes/me.js +1 -2
- package/dist/api/http/routes/memories.d.ts +9 -0
- package/dist/api/http/routes/memories.js +152 -0
- package/dist/api/http/routes/reads.d.ts +1 -137
- package/dist/api/http/routes/reads.js +31 -12
- package/dist/api/http/routes/sessions.js +21 -50
- package/dist/api/http/routes/tenant.js +13 -10
- package/dist/api/http/routes/triggers.d.ts +10 -0
- package/dist/api/http/routes/triggers.js +183 -0
- package/dist/api/openapi.js +69 -47
- package/dist/artifacts/disk.d.ts +10 -10
- package/dist/artifacts/disk.js +27 -27
- package/dist/artifacts/quota.d.ts +1 -1
- package/dist/artifacts/quota.js +2 -2
- package/dist/artifacts/service.js +2 -2
- package/dist/core/flow-host.d.ts +0 -1
- package/dist/gates/egress-credentials.js +1 -1
- package/dist/gates/egress.d.ts +5 -5
- package/dist/gates/egress.js +6 -6
- package/dist/gates/http-tool.js +2 -2
- package/dist/gates/meter.js +22 -5
- package/dist/gates/policy.d.ts +8 -3
- package/dist/gates/policy.js +7 -2
- package/dist/gates/tenant-vaults.d.ts +1 -1
- package/dist/harness/pod.d.ts +2 -0
- package/dist/harness/setup.js +17 -6
- package/dist/harness-api/record.js +11 -0
- package/dist/harness-api/server.js +4 -0
- package/dist/harness-api/start.d.ts +11 -7
- package/dist/harness-api/start.js +49 -5
- package/dist/harness-api/workspace.js +2 -2
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +5 -2
- package/dist/host/main.js +1 -0
- package/dist/keys/keys.d.ts +15 -4
- package/dist/keys/keys.js +16 -0
- package/dist/keys/webhooks.d.ts +25 -0
- package/dist/keys/webhooks.js +62 -0
- package/dist/management-openapi.json +313 -133
- package/dist/mcp/connect.d.ts +2 -2
- package/dist/mcp/connect.js +3 -3
- package/dist/mcp/preview.js +7 -7
- package/dist/memory/service.d.ts +97 -0
- package/dist/memory/service.js +335 -0
- package/dist/memory/tool.d.ts +36 -0
- package/dist/memory/tool.js +84 -0
- package/dist/model/pi-model.js +7 -0
- package/dist/openapi.json +13120 -9034
- package/dist/reads/types.d.ts +3 -3
- package/dist/record/agent.d.ts +27 -0
- package/dist/record/agent.js +46 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/join.js +5 -5
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +3 -3
- package/dist/sandbox/pods/lifecycle.d.ts +6 -6
- package/dist/sandbox/pods/lifecycle.js +6 -6
- package/dist/sandbox/pods/reconcile.js +10 -2
- package/dist/sandbox/resolve.d.ts +3 -3
- package/dist/sandbox/resolve.js +7 -7
- package/dist/sandbox/setup.d.ts +3 -1
- package/dist/sandbox/setup.js +14 -4
- package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
- package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
- package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
- package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
- package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +44 -87
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/schema.d.ts +1408 -123
- package/dist/store/postgres/schema.js +194 -38
- package/dist/store/postgres/store.js +329 -79
- package/dist/store/postgres/tenant.js +1 -2
- package/dist/store/types.d.ts +155 -50
- package/dist/store/types.js +0 -1
- package/dist/tenant/advance.js +20 -8
- package/dist/tenant/agent-manifests.d.ts +72 -0
- package/dist/tenant/agent-manifests.js +359 -0
- package/dist/tenant/agent-versions.d.ts +126 -0
- package/dist/tenant/agent-versions.js +389 -0
- package/dist/tenant/agents.d.ts +230 -0
- package/dist/tenant/{environments.js → agents.js} +464 -320
- package/dist/tenant/auth.d.ts +1 -1
- package/dist/tenant/auth.js +3 -3
- package/dist/tenant/commands.d.ts +63 -2
- package/dist/tenant/commands.js +240 -83
- package/dist/tenant/context.d.ts +46 -19
- package/dist/tenant/context.js +6 -2
- package/dist/tenant/effects.js +7 -16
- package/dist/tenant/identity-file.d.ts +6 -6
- package/dist/tenant/identity-file.js +12 -10
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/limits.d.ts +0 -3
- package/dist/tenant/limits.js +0 -3
- package/dist/tenant/memories.d.ts +38 -0
- package/dist/tenant/memories.js +119 -0
- package/dist/tenant/reset.js +2 -2
- package/dist/tenant/run-grants.d.ts +6 -0
- package/dist/tenant/run-grants.js +4 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +8 -0
- package/dist/tenant/sessions.d.ts +28 -45
- package/dist/tenant/sessions.js +85 -208
- package/dist/tenant/status.js +6 -7
- package/dist/tenant/sweep.js +12 -3
- package/dist/tenant/transcript.d.ts +16 -16
- package/dist/tenant/triggers.d.ts +143 -0
- package/dist/tenant/triggers.js +715 -0
- package/dist/tenant/turn-timing.d.ts +11 -1
- package/dist/tenant/turn-timing.js +3 -1
- package/dist/tenant/types.d.ts +5 -0
- package/dist/tenant/worker.d.ts +2 -0
- package/dist/tenant/worker.js +5 -0
- package/dist/traces/map.d.ts +1 -1
- package/dist/traces/map.js +7 -1
- package/dist/tuning.d.ts +7 -0
- package/dist/tuning.js +19 -0
- package/dist/vault/service.d.ts +13 -8
- package/dist/vault/service.js +61 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -12
- package/dist/api/http/routes/environment-manifests.d.ts +0 -9
- package/dist/api/http/routes/environment-manifests.js +0 -118
- package/dist/api/http/routes/environments.d.ts +0 -11
- package/dist/api/http/routes/environments.js +0 -332
- package/dist/record/environment.d.ts +0 -27
- package/dist/record/environment.js +0 -46
- package/dist/tenant/environment-manifests.d.ts +0 -57
- package/dist/tenant/environment-manifests.js +0 -208
- package/dist/tenant/environments.d.ts +0 -172
package/dist/mcp/connect.d.ts
CHANGED
|
@@ -142,7 +142,7 @@ export declare class McpCallFailed extends Error {
|
|
|
142
142
|
*/
|
|
143
143
|
export declare class CredentialRejected extends McpCallFailed {
|
|
144
144
|
readonly server: string;
|
|
145
|
-
/** Whether a credential of the session's
|
|
145
|
+
/** Whether a credential of the session's Agent was sent. */
|
|
146
146
|
readonly sent: boolean;
|
|
147
147
|
/**
|
|
148
148
|
* The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
|
|
@@ -150,7 +150,7 @@ export declare class CredentialRejected extends McpCallFailed {
|
|
|
150
150
|
*/
|
|
151
151
|
readonly challenge?: string | undefined;
|
|
152
152
|
constructor(server: string,
|
|
153
|
-
/** Whether a credential of the session's
|
|
153
|
+
/** Whether a credential of the session's Agent was sent. */
|
|
154
154
|
sent: boolean,
|
|
155
155
|
/**
|
|
156
156
|
* The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
|
package/dist/mcp/connect.js
CHANGED
|
@@ -49,7 +49,7 @@ export class CredentialRejected extends McpCallFailed {
|
|
|
49
49
|
sent;
|
|
50
50
|
challenge;
|
|
51
51
|
constructor(server,
|
|
52
|
-
/** Whether a credential of the session's
|
|
52
|
+
/** Whether a credential of the session's Agent was sent. */
|
|
53
53
|
sent,
|
|
54
54
|
/**
|
|
55
55
|
* The answer's `WWW-Authenticate`, which may name the server's protected-resource metadata
|
|
@@ -59,8 +59,8 @@ export class CredentialRejected extends McpCallFailed {
|
|
|
59
59
|
super({
|
|
60
60
|
code: "credential_rejected",
|
|
61
61
|
message: sent
|
|
62
|
-
? `The MCP server '${server}' answered HTTP 401: it rejected the credential from the session's
|
|
63
|
-
: `The MCP server '${server}' answered HTTP 401: it needs a credential, and the session's
|
|
62
|
+
? `The MCP server '${server}' answered HTTP 401: it rejected the credential from the session's Agent`
|
|
63
|
+
: `The MCP server '${server}' answered HTTP 401: it needs a credential, and the session's Agent holds none for it`,
|
|
64
64
|
sent: true,
|
|
65
65
|
retryable: false,
|
|
66
66
|
server,
|
package/dist/mcp/preview.js
CHANGED
|
@@ -121,18 +121,18 @@ export async function previewMcpServer(body, options) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
* The credential of
|
|
125
|
-
*
|
|
124
|
+
* The credential of Agent `agentId`'s vault for the URL; none without an Agent (a preview without
|
|
125
|
+
* a credential). An Agent that does not exist is a 404.
|
|
126
126
|
*/
|
|
127
127
|
async function credentialFor(vault, request) {
|
|
128
|
-
if (request.
|
|
128
|
+
if (request.agentId === undefined)
|
|
129
129
|
return { status: "unauthenticated", url: request.url, headers: {} };
|
|
130
|
-
// The 404 of a missing
|
|
131
|
-
await vault.listEnvironmentSecrets(request.
|
|
132
|
-
const result = await vault.authorize({ environmentId: request.
|
|
130
|
+
// The 404 of a missing Agent, before any request leaves.
|
|
131
|
+
await vault.listEnvironmentSecrets(request.agentId);
|
|
132
|
+
const result = await vault.authorize({ environmentId: request.agentId, url: request.url });
|
|
133
133
|
if (result.status !== "refused")
|
|
134
134
|
return result;
|
|
135
|
-
throw new HttpError(409, `
|
|
135
|
+
throw new HttpError(409, `Agent ${request.agentId}'s credential for this URL cannot be used (${result.reason})`, { code: "request_rejected", details: { reason: result.reason, credentialIds: result.credentialIds } });
|
|
136
136
|
}
|
|
137
137
|
/** A preview that listed nothing: `mcp_preview_failed`, with the code a tool call would have had. */
|
|
138
138
|
function failed(server, error, signal, secrets, limits) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type MemoryOperation, type MemorySummary, type MemoryVersion, type MemoryVersionDetail, type RuntimeLimits } from "@nylorun/core/contracts";
|
|
2
|
+
import type { MemoryRow, MemoryVersionRow, Tx } from "../store/types.js";
|
|
3
|
+
/** Who makes a write: `agent:<manifest>@<n>` with its session, turn and run, or an API caller. */
|
|
4
|
+
export interface MemoryAuthor {
|
|
5
|
+
readonly actor: string;
|
|
6
|
+
readonly sessionId?: string;
|
|
7
|
+
readonly turnId?: string;
|
|
8
|
+
readonly triggerRunId?: string;
|
|
9
|
+
}
|
|
10
|
+
/** One of the `memory` tool's write commands, with Anthropic's parameters. */
|
|
11
|
+
export type MemoryCommand = {
|
|
12
|
+
readonly command: "create";
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly fileText: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly command: "str_replace";
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly oldStr: string;
|
|
19
|
+
readonly newStr?: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly command: "insert";
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly insertLine: number;
|
|
24
|
+
readonly insertText: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly command: "delete";
|
|
27
|
+
readonly path: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly command: "rename";
|
|
30
|
+
readonly oldPath: string;
|
|
31
|
+
readonly newPath: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Why a command did not apply. `path_invalid`, `too_large` and `limit` are the Runtime's rules;
|
|
35
|
+
* the rest are Anthropic's ordinary failures (a missing path, an `old_str` that is not there or
|
|
36
|
+
* not unique, an `insert_line` out of range, a rename onto an existing path).
|
|
37
|
+
*/
|
|
38
|
+
export type MemoryRefusal = "memory.path_invalid" | "memory.too_large" | "memory.limit" | "memory.not_found" | "memory.no_match" | "memory.not_unique" | "memory.invalid_line" | "memory.exists";
|
|
39
|
+
/** A memory a command changed: its path, what changed it and its version after. */
|
|
40
|
+
export interface MemoryChange {
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly operation: MemoryOperation;
|
|
43
|
+
readonly version: number;
|
|
44
|
+
}
|
|
45
|
+
/** A refused command or view: the code, and the text for the model. */
|
|
46
|
+
export interface MemoryRefused {
|
|
47
|
+
readonly ok: false;
|
|
48
|
+
readonly code: MemoryRefusal;
|
|
49
|
+
readonly text: string;
|
|
50
|
+
}
|
|
51
|
+
/** What a command came to: the text for the model, and the changes, or why it was refused. */
|
|
52
|
+
export type MemoryResult = {
|
|
53
|
+
readonly ok: true;
|
|
54
|
+
readonly text: string;
|
|
55
|
+
readonly changes: readonly MemoryChange[];
|
|
56
|
+
} | MemoryRefused;
|
|
57
|
+
/** What a view came to: the listing or the text, or why it was refused. */
|
|
58
|
+
export type MemoryViewResult = {
|
|
59
|
+
readonly ok: true;
|
|
60
|
+
readonly text: string;
|
|
61
|
+
} | MemoryRefused;
|
|
62
|
+
/** A new version id: increasing with time, so versions page newest first by id. */
|
|
63
|
+
export declare function newMemoryVersionId(now?: number): string;
|
|
64
|
+
export declare function sha256(text: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* The tool's `view`: a memory's text with line numbers (`viewRange` `[start, end]`, 1-based,
|
|
67
|
+
* `end` -1 for the last line), or a folder's listing two levels deep with sizes. `/memories`
|
|
68
|
+
* lists even when the Agent has no memories.
|
|
69
|
+
*/
|
|
70
|
+
export declare function viewMemory(t: Tx, agentId: string, rawPath: string, viewRange?: readonly [number, number]): Promise<MemoryViewResult>;
|
|
71
|
+
interface Applied {
|
|
72
|
+
readonly t: Tx;
|
|
73
|
+
readonly agentId: string;
|
|
74
|
+
readonly author: MemoryAuthor;
|
|
75
|
+
readonly limits: Pick<RuntimeLimits, "memoryBytes" | "memoriesPerAgent">;
|
|
76
|
+
readonly now: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Applies one write command to Agent `agentId`'s memories in `t`, as `author`, within the
|
|
80
|
+
* Runtime limits. Access is the caller's to check. A command that changes nothing (a `create`
|
|
81
|
+
* with the same text) makes no version.
|
|
82
|
+
*/
|
|
83
|
+
export declare function applyMemoryCommand(t: Tx, agentId: string, command: MemoryCommand, author: MemoryAuthor, limits: Applied["limits"], now?: string): Promise<MemoryResult>;
|
|
84
|
+
/**
|
|
85
|
+
* The index a session pins (`turn.start.memory.index`, design §6.3): a line per memory, newest
|
|
86
|
+
* first, `- <path>`, then ` — <description>` when it has one, within `memoryIndexLines` lines and
|
|
87
|
+
* `memoryIndexBytes` bytes. `truncated` when some memories were left out; the harness tells the
|
|
88
|
+
* model to view folders for the rest.
|
|
89
|
+
*/
|
|
90
|
+
export declare function renderMemoryIndex(t: Tx, agentId: string, limits: Pick<RuntimeLimits, "memoryIndexLines" | "memoryIndexBytes">): Promise<{
|
|
91
|
+
index: string;
|
|
92
|
+
truncated: boolean;
|
|
93
|
+
}>;
|
|
94
|
+
export declare function memorySummaryOf(row: MemoryRow): MemorySummary;
|
|
95
|
+
export declare function memoryVersionOf(row: MemoryVersionRow): MemoryVersion;
|
|
96
|
+
export declare function memoryVersionDetailOf(row: MemoryVersionRow): MemoryVersionDetail;
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent memory (track T5, D72): the one place memories change. The `memory` tool's commands
|
|
3
|
+
* (Anthropic's memory tool: `create`, `str_replace`, `insert`, `delete`, `rename`) and the API's
|
|
4
|
+
* writes both go through `applyMemoryCommand`, so paths, limits, versions and locks are the same
|
|
5
|
+
* for a model and for a person. Reads are `viewMemory` (the tool's `view`) and the API's.
|
|
6
|
+
*
|
|
7
|
+
* Every change is a new row in `memory_versions` attributed to who made it; the Runtime never
|
|
8
|
+
* rewrites what was written. A write takes a lock on each path it touches, in path order, so
|
|
9
|
+
* writes to different memories never wait on each other. Answers to the model are Anthropic's
|
|
10
|
+
* result strings; a refusal also carries a `memory.*` code.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
import { MEMORY_ROOT, memoryDescriptionOf, memoryPathOf, memoryPathWithin, } from "@nylorun/core/contracts";
|
|
14
|
+
/** A new version id: increasing with time, so versions page newest first by id. */
|
|
15
|
+
export function newMemoryVersionId(now = Date.now()) {
|
|
16
|
+
return `mv_${now.toString(36).padStart(9, "0")}${randomBytes(8).toString("hex")}`;
|
|
17
|
+
}
|
|
18
|
+
export function sha256(text) {
|
|
19
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
20
|
+
}
|
|
21
|
+
const refused = (code, text) => ({ ok: false, code, text });
|
|
22
|
+
const missing = (path) => refused("memory.not_found", `The path ${path} does not exist. Please provide a valid path.`);
|
|
23
|
+
function pathOf(raw, kind) {
|
|
24
|
+
const parsed = memoryPathOf(raw, kind);
|
|
25
|
+
return "error" in parsed ? refused("memory.path_invalid", parsed.error) : parsed;
|
|
26
|
+
}
|
|
27
|
+
/** A file's text with Anthropic's line numbers: six columns, a tab, the line. */
|
|
28
|
+
function numbered(text, start = 1) {
|
|
29
|
+
return text
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line, index) => `${String(index + start).padStart(6, " ")}\t${line}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
}
|
|
34
|
+
function sizeOf(bytes) {
|
|
35
|
+
if (bytes < 1024)
|
|
36
|
+
return `${bytes}B`;
|
|
37
|
+
if (bytes < 1024 * 1024)
|
|
38
|
+
return `${(bytes / 1024).toFixed(1)}K`;
|
|
39
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The tool's `view`: a memory's text with line numbers (`viewRange` `[start, end]`, 1-based,
|
|
43
|
+
* `end` -1 for the last line), or a folder's listing two levels deep with sizes. `/memories`
|
|
44
|
+
* lists even when the Agent has no memories.
|
|
45
|
+
*/
|
|
46
|
+
export async function viewMemory(t, agentId, rawPath, viewRange) {
|
|
47
|
+
const parsed = pathOf(rawPath, "any");
|
|
48
|
+
if (!("path" in parsed))
|
|
49
|
+
return parsed;
|
|
50
|
+
const path = parsed.path;
|
|
51
|
+
const row = path.endsWith(".md") ? await t.memory(agentId, path) : undefined;
|
|
52
|
+
if (row) {
|
|
53
|
+
const lines = row.content.split("\n");
|
|
54
|
+
if (viewRange === undefined)
|
|
55
|
+
return { ok: true, text: `Here's the content of ${path} with line numbers:\n${numbered(row.content)}` };
|
|
56
|
+
const [start, end] = viewRange;
|
|
57
|
+
const last = end === -1 ? lines.length : end;
|
|
58
|
+
if (!Number.isInteger(start) || !Number.isInteger(last) || start < 1 || start > lines.length || last < start || last > lines.length)
|
|
59
|
+
return refused("memory.invalid_line", `Invalid view_range [${start}, ${end}]: ${path} has ${lines.length} lines.`);
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
text: `Here's the content of ${path} with line numbers:\n${numbered(lines.slice(start - 1, last).join("\n"), start)}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const rows = await t.memories(agentId, { folder: path });
|
|
66
|
+
if (rows.length === 0 && path !== MEMORY_ROOT)
|
|
67
|
+
return missing(path);
|
|
68
|
+
// Folders are the prefixes of memory paths: list what is one or two levels below `path`.
|
|
69
|
+
const sizes = new Map();
|
|
70
|
+
for (const memory of rows) {
|
|
71
|
+
const rest = memory.path.slice(path.length + 1).split("/");
|
|
72
|
+
for (let depth = 1; depth <= Math.min(2, rest.length); depth++) {
|
|
73
|
+
const entry = `${path}/${rest.slice(0, depth).join("/")}`;
|
|
74
|
+
sizes.set(entry, (sizes.get(entry) ?? 0) + memory.bytes);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const total = rows.reduce((sum, memory) => sum + memory.bytes, 0);
|
|
78
|
+
const lines = [`${sizeOf(total)}\t${path}`, ...[...sizes.keys()].sort().map((entry) => `${sizeOf(sizes.get(entry))}\t${entry}`)];
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
text: `Here're the files and directories up to 2 levels deep in ${path}, excluding hidden items and node_modules:\n${lines.join("\n")}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function tooLarge(content, limits) {
|
|
85
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
86
|
+
return bytes > limits.memoryBytes
|
|
87
|
+
? refused("memory.too_large", `A memory is at most ${limits.memoryBytes} bytes (the Runtime limit memoryBytes); this one would be ${bytes}. Split it into smaller files.`)
|
|
88
|
+
: undefined;
|
|
89
|
+
}
|
|
90
|
+
/** Writes `content` at `path` (row `before`, locked, when it exists) and records the version. */
|
|
91
|
+
async function write(a, path, before, content, operation, fromPath) {
|
|
92
|
+
const contentSha256 = sha256(content);
|
|
93
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
94
|
+
const version = (before?.version ?? 0) + 1;
|
|
95
|
+
await a.t.putMemory({
|
|
96
|
+
agentId: a.agentId,
|
|
97
|
+
path,
|
|
98
|
+
content,
|
|
99
|
+
contentSha256,
|
|
100
|
+
bytes,
|
|
101
|
+
description: memoryDescriptionOf(content),
|
|
102
|
+
version,
|
|
103
|
+
updatedAt: a.now,
|
|
104
|
+
});
|
|
105
|
+
await a.t.insertMemoryVersion(versionRow(a, { path, fromPath: fromPath ?? null, operation, content, contentSha256, bytes }));
|
|
106
|
+
return { path, operation, version };
|
|
107
|
+
}
|
|
108
|
+
function versionRow(a, change) {
|
|
109
|
+
return {
|
|
110
|
+
id: newMemoryVersionId(Date.parse(a.now)),
|
|
111
|
+
agentId: a.agentId,
|
|
112
|
+
...change,
|
|
113
|
+
actor: a.author.actor,
|
|
114
|
+
sessionId: a.author.sessionId ?? null,
|
|
115
|
+
turnId: a.author.turnId ?? null,
|
|
116
|
+
triggerRunId: a.author.triggerRunId ?? null,
|
|
117
|
+
createdAt: a.now,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Deletes `row` (locked) and records the delete. */
|
|
121
|
+
async function remove(a, row) {
|
|
122
|
+
await a.t.deleteMemory(a.agentId, row.path);
|
|
123
|
+
await a.t.insertMemoryVersion(versionRow(a, { path: row.path, fromPath: null, operation: "delete", content: null, contentSha256: null, bytes: null }));
|
|
124
|
+
return { path: row.path, operation: "delete", version: row.version + 1 };
|
|
125
|
+
}
|
|
126
|
+
/** Locks the memories at `paths` (in path order, so two writes never wait on each other's locks). */
|
|
127
|
+
async function lockPaths(t, agentId, paths) {
|
|
128
|
+
const rows = new Map();
|
|
129
|
+
for (const path of [...new Set(paths)].sort())
|
|
130
|
+
rows.set(path, await t.memory(agentId, path, { lock: true }));
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Applies one write command to Agent `agentId`'s memories in `t`, as `author`, within the
|
|
135
|
+
* Runtime limits. Access is the caller's to check. A command that changes nothing (a `create`
|
|
136
|
+
* with the same text) makes no version.
|
|
137
|
+
*/
|
|
138
|
+
export async function applyMemoryCommand(t, agentId, command, author, limits, now = new Date().toISOString()) {
|
|
139
|
+
const a = { t, agentId, author, limits, now };
|
|
140
|
+
switch (command.command) {
|
|
141
|
+
case "create":
|
|
142
|
+
return create(a, command.path, command.fileText);
|
|
143
|
+
case "str_replace":
|
|
144
|
+
return strReplace(a, command.path, command.oldStr, command.newStr ?? "");
|
|
145
|
+
case "insert":
|
|
146
|
+
return insert(a, command.path, command.insertLine, command.insertText);
|
|
147
|
+
case "delete":
|
|
148
|
+
return del(a, command.path);
|
|
149
|
+
case "rename":
|
|
150
|
+
return rename(a, command.oldPath, command.newPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function create(a, rawPath, content) {
|
|
154
|
+
const parsed = pathOf(rawPath, "file");
|
|
155
|
+
if (!("path" in parsed))
|
|
156
|
+
return parsed;
|
|
157
|
+
const path = parsed.path;
|
|
158
|
+
const big = tooLarge(content, a.limits);
|
|
159
|
+
if (big)
|
|
160
|
+
return big;
|
|
161
|
+
const before = (await lockPaths(a.t, a.agentId, [path])).get(path);
|
|
162
|
+
// A memory can't share its path with a folder of memories.
|
|
163
|
+
if (!before && (await a.t.memories(a.agentId, { folder: path, limit: 1 })).length > 0)
|
|
164
|
+
return refused("memory.exists", `${path} is a folder of memories`);
|
|
165
|
+
if (before?.content === content)
|
|
166
|
+
return { ok: true, text: `File created successfully at: ${path}`, changes: [] };
|
|
167
|
+
if (!before && (await a.t.countMemories(a.agentId)) >= a.limits.memoriesPerAgent)
|
|
168
|
+
return refused("memory.limit", `This Agent holds ${a.limits.memoriesPerAgent} memories, the most the Runtime limit memoriesPerAgent allows. Delete or merge some first.`);
|
|
169
|
+
const change = await write(a, path, before, content, "create");
|
|
170
|
+
return { ok: true, text: `File created successfully at: ${path}`, changes: [change] };
|
|
171
|
+
}
|
|
172
|
+
async function strReplace(a, rawPath, oldStr, newStr) {
|
|
173
|
+
const parsed = pathOf(rawPath, "file");
|
|
174
|
+
if (!("path" in parsed))
|
|
175
|
+
return parsed;
|
|
176
|
+
const path = parsed.path;
|
|
177
|
+
const before = (await lockPaths(a.t, a.agentId, [path])).get(path);
|
|
178
|
+
if (!before)
|
|
179
|
+
return missing(path);
|
|
180
|
+
if (oldStr === "")
|
|
181
|
+
return refused("memory.no_match", "old_str is empty: give the text to replace.");
|
|
182
|
+
const lines = [];
|
|
183
|
+
for (let at = before.content.indexOf(oldStr); at !== -1; at = before.content.indexOf(oldStr, at + 1))
|
|
184
|
+
lines.push(before.content.slice(0, at).split("\n").length);
|
|
185
|
+
if (lines.length === 0)
|
|
186
|
+
return refused("memory.no_match", `No replacement was performed, old_str \`${oldStr}\` did not appear verbatim in ${path}.`);
|
|
187
|
+
if (lines.length > 1)
|
|
188
|
+
return refused("memory.not_unique", `No replacement was performed. Multiple occurrences of old_str \`${oldStr}\` in lines: ${lines.join(", ")}. Please ensure it is unique`);
|
|
189
|
+
const content = before.content.replace(oldStr, () => newStr);
|
|
190
|
+
const big = tooLarge(content, a.limits);
|
|
191
|
+
if (big)
|
|
192
|
+
return big;
|
|
193
|
+
const change = await write(a, path, before, content, "str_replace");
|
|
194
|
+
// Anthropic answers with the edited lines around the change.
|
|
195
|
+
const line = lines[0];
|
|
196
|
+
const all = content.split("\n");
|
|
197
|
+
const from = Math.max(1, line - 4);
|
|
198
|
+
const to = Math.min(all.length, line + newStr.split("\n").length + 3);
|
|
199
|
+
return {
|
|
200
|
+
ok: true,
|
|
201
|
+
text: `The memory file has been edited.\n${numbered(all.slice(from - 1, to).join("\n"), from)}`,
|
|
202
|
+
changes: [change],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async function insert(a, rawPath, insertLine, insertText) {
|
|
206
|
+
const parsed = pathOf(rawPath, "file");
|
|
207
|
+
if (!("path" in parsed))
|
|
208
|
+
return parsed;
|
|
209
|
+
const path = parsed.path;
|
|
210
|
+
const before = (await lockPaths(a.t, a.agentId, [path])).get(path);
|
|
211
|
+
if (!before)
|
|
212
|
+
return missing(path);
|
|
213
|
+
const lines = before.content.split("\n");
|
|
214
|
+
if (!Number.isInteger(insertLine) || insertLine < 0 || insertLine > lines.length)
|
|
215
|
+
return refused("memory.invalid_line", `Invalid \`insert_line\` parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${lines.length}]`);
|
|
216
|
+
const added = insertText.endsWith("\n") ? insertText.slice(0, -1) : insertText;
|
|
217
|
+
lines.splice(insertLine, 0, ...added.split("\n"));
|
|
218
|
+
const content = lines.join("\n");
|
|
219
|
+
const big = tooLarge(content, a.limits);
|
|
220
|
+
if (big)
|
|
221
|
+
return big;
|
|
222
|
+
const change = await write(a, path, before, content, "insert");
|
|
223
|
+
return { ok: true, text: `The file ${path} has been edited.`, changes: [change] };
|
|
224
|
+
}
|
|
225
|
+
async function del(a, rawPath) {
|
|
226
|
+
const parsed = pathOf(rawPath, "any");
|
|
227
|
+
if (!("path" in parsed))
|
|
228
|
+
return parsed;
|
|
229
|
+
const path = parsed.path;
|
|
230
|
+
if (path === MEMORY_ROOT)
|
|
231
|
+
return refused("memory.path_invalid", `${MEMORY_ROOT} itself can't be deleted: delete what is in it.`);
|
|
232
|
+
const file = path.endsWith(".md") ? (await lockPaths(a.t, a.agentId, [path])).get(path) : undefined;
|
|
233
|
+
const rows = file ? [file] : await a.t.memories(a.agentId, { folder: path, lock: true });
|
|
234
|
+
if (rows.length === 0)
|
|
235
|
+
return missing(path);
|
|
236
|
+
const changes = [];
|
|
237
|
+
for (const row of rows)
|
|
238
|
+
changes.push(await remove(a, row));
|
|
239
|
+
return { ok: true, text: `Successfully deleted ${path}`, changes };
|
|
240
|
+
}
|
|
241
|
+
async function rename(a, rawOld, rawNew) {
|
|
242
|
+
const from = pathOf(rawOld, "any");
|
|
243
|
+
if (!("path" in from))
|
|
244
|
+
return from;
|
|
245
|
+
const oldPath = from.path;
|
|
246
|
+
if (oldPath === MEMORY_ROOT)
|
|
247
|
+
return refused("memory.path_invalid", `${MEMORY_ROOT} itself can't be renamed.`);
|
|
248
|
+
const isFile = oldPath.endsWith(".md") && (await a.t.memory(a.agentId, oldPath)) !== undefined;
|
|
249
|
+
const to = pathOf(rawNew, isFile ? "file" : "any");
|
|
250
|
+
if (!("path" in to))
|
|
251
|
+
return to;
|
|
252
|
+
const newPath = to.path;
|
|
253
|
+
if (newPath === MEMORY_ROOT || memoryPathWithin(newPath, oldPath))
|
|
254
|
+
return refused("memory.path_invalid", `Can't rename ${oldPath} to ${newPath}: it would move into itself.`);
|
|
255
|
+
if (isFile) {
|
|
256
|
+
const rows = await lockPaths(a.t, a.agentId, [oldPath, newPath]);
|
|
257
|
+
const row = rows.get(oldPath);
|
|
258
|
+
if (!row)
|
|
259
|
+
return missing(oldPath);
|
|
260
|
+
if (rows.get(newPath) || (await a.t.memories(a.agentId, { folder: newPath, limit: 1 })).length > 0)
|
|
261
|
+
return refused("memory.exists", `The destination ${newPath} already exists`);
|
|
262
|
+
await a.t.deleteMemory(a.agentId, oldPath);
|
|
263
|
+
const change = await write(a, newPath, { ...row, version: 0 }, row.content, "rename", oldPath);
|
|
264
|
+
return { ok: true, text: `Successfully renamed ${oldPath} to ${newPath}`, changes: [change] };
|
|
265
|
+
}
|
|
266
|
+
const rows = await a.t.memories(a.agentId, { folder: oldPath, lock: true });
|
|
267
|
+
if (rows.length === 0)
|
|
268
|
+
return missing(oldPath);
|
|
269
|
+
const targets = rows.map((row) => `${newPath}${row.path.slice(oldPath.length)}`);
|
|
270
|
+
for (const target of targets)
|
|
271
|
+
if ("error" in memoryPathOf(target, "file"))
|
|
272
|
+
return refused("memory.path_invalid", `Renaming ${oldPath} to ${newPath} would make ${target}, which is too long or invalid.`);
|
|
273
|
+
const taken = await lockPaths(a.t, a.agentId, [newPath, ...targets]);
|
|
274
|
+
if ([...taken.values()].some(Boolean) || (await a.t.memories(a.agentId, { folder: newPath, limit: 1 })).length > 0)
|
|
275
|
+
return refused("memory.exists", `The destination ${newPath} already exists`);
|
|
276
|
+
const changes = [];
|
|
277
|
+
for (const [index, row] of rows.entries()) {
|
|
278
|
+
await a.t.deleteMemory(a.agentId, row.path);
|
|
279
|
+
changes.push(await write(a, targets[index], { ...row, version: 0 }, row.content, "rename", row.path));
|
|
280
|
+
}
|
|
281
|
+
return { ok: true, text: `Successfully renamed ${oldPath} to ${newPath}`, changes };
|
|
282
|
+
}
|
|
283
|
+
// --- the pinned index ------------------------------------------------------------
|
|
284
|
+
/**
|
|
285
|
+
* The index a session pins (`turn.start.memory.index`, design §6.3): a line per memory, newest
|
|
286
|
+
* first, `- <path>`, then ` — <description>` when it has one, within `memoryIndexLines` lines and
|
|
287
|
+
* `memoryIndexBytes` bytes. `truncated` when some memories were left out; the harness tells the
|
|
288
|
+
* model to view folders for the rest.
|
|
289
|
+
*/
|
|
290
|
+
export async function renderMemoryIndex(t, agentId, limits) {
|
|
291
|
+
const rows = await t.recentMemories(agentId, limits.memoryIndexLines + 1);
|
|
292
|
+
const lines = [];
|
|
293
|
+
let bytes = 0;
|
|
294
|
+
let truncated = rows.length > limits.memoryIndexLines;
|
|
295
|
+
for (const row of rows.slice(0, limits.memoryIndexLines)) {
|
|
296
|
+
const line = row.description ? `- ${row.path} — ${row.description}` : `- ${row.path}`;
|
|
297
|
+
const size = Buffer.byteLength(line, "utf8") + 1;
|
|
298
|
+
if (bytes + size > limits.memoryIndexBytes) {
|
|
299
|
+
truncated = true;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
lines.push(line);
|
|
303
|
+
bytes += size;
|
|
304
|
+
}
|
|
305
|
+
return { index: lines.join("\n"), truncated };
|
|
306
|
+
}
|
|
307
|
+
// --- views ---------------------------------------------------------------------
|
|
308
|
+
export function memorySummaryOf(row) {
|
|
309
|
+
return {
|
|
310
|
+
path: row.path,
|
|
311
|
+
bytes: row.bytes,
|
|
312
|
+
contentSha256: row.contentSha256,
|
|
313
|
+
description: row.description,
|
|
314
|
+
version: row.version,
|
|
315
|
+
updatedAt: row.updatedAt,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
export function memoryVersionOf(row) {
|
|
319
|
+
return {
|
|
320
|
+
id: row.id,
|
|
321
|
+
path: row.path,
|
|
322
|
+
fromPath: row.fromPath,
|
|
323
|
+
operation: row.operation,
|
|
324
|
+
bytes: row.bytes,
|
|
325
|
+
contentSha256: row.contentSha256,
|
|
326
|
+
actor: row.actor,
|
|
327
|
+
sessionId: row.sessionId,
|
|
328
|
+
turnId: row.turnId,
|
|
329
|
+
triggerRunId: row.triggerRunId,
|
|
330
|
+
createdAt: row.createdAt,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export function memoryVersionDetailOf(row) {
|
|
334
|
+
return { ...memoryVersionOf(row), content: row.content };
|
|
335
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `memory` tool's calls (track T5, D72): a `runtime` effect, operation `memory`, which core
|
|
3
|
+
* serves in the transaction that journals the call, as it does `schedule_wake`. So a replayed turn
|
|
4
|
+
* applies a write once and sees the same `view`, and a BYO harness gets the same answers by sending
|
|
5
|
+
* the same target. The input is Anthropic's memory tool's: `command` and its parameters.
|
|
6
|
+
*
|
|
7
|
+
* Core checks every call, whichever harness sent it (our engine runs the same `checkMemoryCall`
|
|
8
|
+
* before it sends one): a call missing a command's parameter, or with a parameter of the wrong
|
|
9
|
+
* type, fails with Anthropic-style text (`Error: create requires file_text.`) and changes nothing. Writes need `access: "read_write"` in the version the session runs and the
|
|
10
|
+
* root agent: a sub-agent's call (one that names its `agent`) may only view (`memory.forbidden`).
|
|
11
|
+
*/
|
|
12
|
+
import { type AgentMemorySettings } from "@nylorun/core/contracts";
|
|
13
|
+
import type { Tx } from "../store/types.js";
|
|
14
|
+
import type { Session } from "../tenant/context.js";
|
|
15
|
+
type Outcome = {
|
|
16
|
+
kind: "completed";
|
|
17
|
+
output: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "failed";
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
/** `harness.memory` of the agent manifest version session `s` runs, when it sets one. */
|
|
24
|
+
export declare function memorySettingsOf(t: Tx, s: Pick<Session, "definition">): Promise<AgentMemorySettings | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* Serves one `memory` call of session `s`'s active turn in `t`: a view, or a write recorded as a
|
|
27
|
+
* version attributed to the turn (`agent:<manifest>@<version>`, the session, turn and the trigger
|
|
28
|
+
* run that started it), with `memory.updated` in the session and `agent.memory_updated` on the
|
|
29
|
+
* Agent's stream when something changed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function serveMemoryTool(t: Tx, s: Session, request: {
|
|
32
|
+
effectId: string;
|
|
33
|
+
input?: unknown;
|
|
34
|
+
agent?: unknown;
|
|
35
|
+
}): Promise<Outcome>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `memory` tool's calls (track T5, D72): a `runtime` effect, operation `memory`, which core
|
|
3
|
+
* serves in the transaction that journals the call, as it does `schedule_wake`. So a replayed turn
|
|
4
|
+
* applies a write once and sees the same `view`, and a BYO harness gets the same answers by sending
|
|
5
|
+
* the same target. The input is Anthropic's memory tool's: `command` and its parameters.
|
|
6
|
+
*
|
|
7
|
+
* Core checks every call, whichever harness sent it (our engine runs the same `checkMemoryCall`
|
|
8
|
+
* before it sends one): a call missing a command's parameter, or with a parameter of the wrong
|
|
9
|
+
* type, fails with Anthropic-style text (`Error: create requires file_text.`) and changes nothing. Writes need `access: "read_write"` in the version the session runs and the
|
|
10
|
+
* root agent: a sub-agent's call (one that names its `agent`) may only view (`memory.forbidden`).
|
|
11
|
+
*/
|
|
12
|
+
import { checkMemoryCall } from "@nylorun/core/contracts";
|
|
13
|
+
import { readRuntimeLimits } from "../tenant/runtime-limits.js";
|
|
14
|
+
import { applyMemoryCommand, viewMemory } from "./service.js";
|
|
15
|
+
/** `harness.memory` of the agent manifest version session `s` runs, when it sets one. */
|
|
16
|
+
export async function memorySettingsOf(t, s) {
|
|
17
|
+
if (!s.definition)
|
|
18
|
+
return undefined;
|
|
19
|
+
const version = await t.agentManifest(s.definition.name, s.definition.version);
|
|
20
|
+
const harness = version?.body.harness;
|
|
21
|
+
return harness?.kind === "workflow" ? undefined : harness?.memory;
|
|
22
|
+
}
|
|
23
|
+
function commandOf(command, args) {
|
|
24
|
+
switch (command) {
|
|
25
|
+
case "create":
|
|
26
|
+
return { command, path: args.path, fileText: args.file_text };
|
|
27
|
+
case "str_replace":
|
|
28
|
+
return {
|
|
29
|
+
command,
|
|
30
|
+
path: args.path,
|
|
31
|
+
oldStr: args.old_str,
|
|
32
|
+
...(typeof args.new_str === "string" ? { newStr: args.new_str } : {}),
|
|
33
|
+
};
|
|
34
|
+
case "insert":
|
|
35
|
+
return { command, path: args.path, insertLine: Number(args.insert_line), insertText: args.insert_text };
|
|
36
|
+
case "delete":
|
|
37
|
+
return { command, path: args.path };
|
|
38
|
+
case "rename":
|
|
39
|
+
return { command, oldPath: args.old_path, newPath: args.new_path };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Serves one `memory` call of session `s`'s active turn in `t`: a view, or a write recorded as a
|
|
44
|
+
* version attributed to the turn (`agent:<manifest>@<version>`, the session, turn and the trigger
|
|
45
|
+
* run that started it), with `memory.updated` in the session and `agent.memory_updated` on the
|
|
46
|
+
* Agent's stream when something changed.
|
|
47
|
+
*/
|
|
48
|
+
export async function serveMemoryTool(t, s, request) {
|
|
49
|
+
const settings = await memorySettingsOf(t, s);
|
|
50
|
+
if (!settings)
|
|
51
|
+
return { kind: "failed", code: "tool.unavailable", message: "This session's agent has no memory (harness.memory)." };
|
|
52
|
+
const parsed = checkMemoryCall(request.input);
|
|
53
|
+
if ("error" in parsed)
|
|
54
|
+
return { kind: "failed", code: "tool.invalid-input", message: parsed.error };
|
|
55
|
+
const { command, args } = parsed;
|
|
56
|
+
if (command === "view") {
|
|
57
|
+
const range = Array.isArray(args.view_range) ? [Number(args.view_range[0]), Number(args.view_range[1])] : undefined;
|
|
58
|
+
const viewed = await viewMemory(t, s.agentId, args.path, range);
|
|
59
|
+
return viewed.ok ? { kind: "completed", output: viewed.text } : { kind: "failed", code: viewed.code, message: viewed.text };
|
|
60
|
+
}
|
|
61
|
+
if ((settings.access ?? "read_write") === "read_only" || request.agent !== undefined)
|
|
62
|
+
return {
|
|
63
|
+
kind: "failed",
|
|
64
|
+
code: "memory.forbidden",
|
|
65
|
+
message: request.agent !== undefined
|
|
66
|
+
? "Error: memory is read-only for a sub-agent; only the main agent writes it."
|
|
67
|
+
: "Error: this agent's memory is read-only (harness.memory.access); view it, but don't change it.",
|
|
68
|
+
};
|
|
69
|
+
const turnId = s.activeTurnId;
|
|
70
|
+
const result = await applyMemoryCommand(t, s.agentId, commandOf(command, args), {
|
|
71
|
+
actor: `agent:${s.definition.name}@${s.definition.version}`,
|
|
72
|
+
sessionId: s.id,
|
|
73
|
+
turnId,
|
|
74
|
+
...(s.turnCause ? { triggerRunId: s.turnCause.runId } : {}),
|
|
75
|
+
}, await readRuntimeLimits(t));
|
|
76
|
+
if (!result.ok)
|
|
77
|
+
return { kind: "failed", code: result.code, message: result.text };
|
|
78
|
+
if (result.changes.length > 0) {
|
|
79
|
+
const changes = result.changes.map((change) => ({ ...change }));
|
|
80
|
+
await t.event(s.id, turnId, "memory.updated", { effectId: request.effectId, changes });
|
|
81
|
+
await t.agentEvent(s.agentId, "agent.memory_updated", { effectId: request.effectId, changes, sessionId: s.id, turnId });
|
|
82
|
+
}
|
|
83
|
+
return { kind: "completed", output: result.text };
|
|
84
|
+
}
|
package/dist/model/pi-model.js
CHANGED
|
@@ -225,6 +225,13 @@ export function piModel(options = {}) {
|
|
|
225
225
|
// pi-ai initializes missing counters to zero; reporting provenance is unknown.
|
|
226
226
|
tokensReported: null,
|
|
227
227
|
costKnown: !selection.custom,
|
|
228
|
+
// The cost by token kind, at the catalog's prices: the usage ledger keeps it.
|
|
229
|
+
costSplit: {
|
|
230
|
+
input: response.usage.cost.input,
|
|
231
|
+
cacheRead: response.usage.cost.cacheRead,
|
|
232
|
+
cacheWrite: response.usage.cost.cacheWrite,
|
|
233
|
+
output: response.usage.cost.output,
|
|
234
|
+
},
|
|
228
235
|
producer: {
|
|
229
236
|
provider: selected.provider,
|
|
230
237
|
api: selected.api,
|