privateer-agent 0.12.8 → 0.12.11

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/src/session.ts CHANGED
@@ -16,8 +16,18 @@ import {
16
16
  SessionManager,
17
17
  SettingsManager,
18
18
  ModelRegistry,
19
- AuthStorage,
19
+ ModelRuntime,
20
20
  } from "@earendil-works/pi-coding-agent";
21
+ // From pi-ai, deliberately, not pi-coding-agent's `AuthStorage.inMemory()`. 0.84
22
+ // stopped exporting AuthStorage and our pi patch puts the export back — which is
23
+ // fine for the one caller that genuinely needs the file-backed store
24
+ // (providers/piAuthStore.ts) and wrong here, because patches are best-effort by
25
+ // design: `bin/apply-patches.mjs` degrades to stock pi when it cannot write to
26
+ // node_modules (the `sudo npm i -g` case it calls out). A static import of a
27
+ // patched-in export turns that graceful degradation into a hard ERR_IMPORT crash
28
+ // on every session start. pi-ai exports this store unpatched and it implements the
29
+ // same CredentialStore interface, so nothing about the session changes.
30
+ import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
21
31
 
22
32
  import { agentDir as defaultAgentDir } from "./config/paths.ts";
23
33
  import { createEngineEventAdapter } from "./bridge/engineAdapter.ts";
@@ -46,15 +56,23 @@ export interface PrivateerSession {
46
56
  export async function createSession(opts: CreateSessionOptions): Promise<PrivateerSession> {
47
57
  const AGENT_DIR = opts.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? defaultAgentDir();
48
58
 
49
- const authStorage = AuthStorage.inMemory();
50
- const modelRegistry = ModelRegistry.create(authStorage, `${AGENT_DIR}/models.json`);
51
- if (typeof (modelRegistry as any).refresh === "function") await (modelRegistry as any).refresh();
52
- else if (typeof (modelRegistry as any).loadModels === "function")
53
- await (modelRegistry as any).loadModels();
59
+ // pi 0.84: ModelRegistry.create(authStorage, modelsPath) is gone. The registry is now
60
+ // a synchronous facade over a ModelRuntime, and the runtime is what owns credentials —
61
+ // so the store this session used to hand to the registry is injected into the runtime
62
+ // instead. Still in-memory: a headless session must not touch the machine's auth.json.
63
+ const authStorage = new InMemoryCredentialStore();
64
+ const modelRuntime = await ModelRuntime.create({
65
+ credentials: authStorage,
66
+ modelsPath: `${AGENT_DIR}/models.json`,
67
+ });
68
+ const modelRegistry = new ModelRegistry(modelRuntime);
69
+ // Explicit, and awaited: find() below is synchronous, so models.json has to be loaded
70
+ // before it runs or the lookup fails on a cold cache.
71
+ await modelRegistry.refresh();
54
72
 
55
73
  const model = modelRegistry.find(opts.provider, opts.modelId);
56
74
  if (!model) {
57
- const ids = ((modelRegistry as any).getAll?.() ?? []).map(
75
+ const ids = (modelRegistry.getAll() ?? []).map(
58
76
  (m: any) => `${m.provider}/${m.id}`,
59
77
  );
60
78
  throw new Error(
@@ -75,8 +93,7 @@ export async function createSession(opts: CreateSessionOptions): Promise<Private
75
93
  cwd: opts.cwd,
76
94
  agentDir: AGENT_DIR,
77
95
  model,
78
- authStorage,
79
- modelRegistry,
96
+ modelRuntime,
80
97
  settingsManager,
81
98
  sessionManager: SessionManager.inMemory(opts.cwd),
82
99
  resourceLoader: loader,
@@ -0,0 +1,66 @@
1
+ // The `attach_to_result` tool — hand a file from disk to the Privateer Inbox
2
+ // alongside the answer this unattended run is about to deliver.
3
+ //
4
+ // The sibling of `send_file_to_client`, for the case that tool cannot serve: there
5
+ // is no controller attached and there won't be one. A routine that fires at 7am,
6
+ // or a task submitted from a phone that then went in a pocket, finishes into the
7
+ // cloud outbox — so its media has to travel the same way, sealed to the account
8
+ // key and collected when the app next syncs (src/routines/resultMedia.ts explains
9
+ // the transport; the ceilings live there too).
10
+ //
11
+ // Staging only. Nothing is read, sealed or uploaded here — the run may still fail
12
+ // after this call, and delivery decides what actually goes. That also keeps the
13
+ // tool cheap enough for a model to use freely: it is a promise about the answer,
14
+ // not an upload.
15
+
16
+ import { Type } from "typebox";
17
+ import type { ResultMedia } from "../routines/resultMedia.ts";
18
+ import { MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES } from "../routines/resultMedia.ts";
19
+
20
+ function text(t: string) {
21
+ return { content: [{ type: "text", text: t }], details: {} };
22
+ }
23
+
24
+ export const ATTACH_RESULT_TOOL = "attach_to_result";
25
+
26
+ export function makeAttachResultTool(media: ResultMedia) {
27
+ return {
28
+ name: ATTACH_RESULT_TOOL,
29
+ label: "Attach to Result",
30
+ description:
31
+ "Attach a file from disk — an image, video, audio clip or document — to the result this run " +
32
+ "delivers to the user's Privateer Inbox, where they can view, save and share it on their device. " +
33
+ "Use it for anything you generated or found that is better seen than described; the user is NOT " +
34
+ "at this machine, so a file path in the answer is not something they can open. Attach the file " +
35
+ `itself rather than pasting base64 into the answer. Up to ${MAX_ATTACHMENTS} files per result, ` +
36
+ `${Math.round(MAX_ATTACHMENT_BYTES / 1048576)} MB each. Attaching the same path twice replaces the earlier entry.`,
37
+ parameters: Type.Object({
38
+ path: Type.String({ description: "Path of the file to attach, relative to cwd or absolute." }),
39
+ caption: Type.Optional(
40
+ Type.String({ description: "One short line shown under the attachment in the app." }),
41
+ ),
42
+ }),
43
+ async execute(
44
+ _toolCallId: string,
45
+ params: { path: string; caption?: string },
46
+ _signal?: AbortSignal,
47
+ _onUpdate?: unknown,
48
+ ctx?: { cwd?: string },
49
+ ) {
50
+ const res = media.stage(params.path, ctx?.cwd ?? process.cwd(), params.caption);
51
+ if (!res.ok) return text(`Not attached — ${res.reason}`);
52
+ const kb = Math.max(1, Math.round(res.item.size / 1024));
53
+ return text(
54
+ `Attached ${res.item.name} (${kb} KB, ${res.item.mediaType}). It will be delivered with this result. ` +
55
+ `Write the answer as if the user can see it — don't paste the file path as the way to open it.`,
56
+ );
57
+ },
58
+ };
59
+ }
60
+
61
+ /** Registers the tool for a session that has a staging area. */
62
+ export function makeAttachResultTools(media: ResultMedia) {
63
+ return function attachResultTools(pi: any): void {
64
+ pi.registerTool?.(makeAttachResultTool(media));
65
+ };
66
+ }
@@ -9,12 +9,12 @@
9
9
  // user has explicitly opted into non-ZDR media (`ZDR_MEDIA_BLOCKED`), and these tools
10
10
  // surface that refusal verbatim rather than papering over it.
11
11
  //
12
- // WHAT THIS COSTS, HONESTLY. Generation is NOT end-to-end encrypted and cannot be. The
12
+ // WHAT THIS COSTS, HONESTLY. Generation is NOT encrypted and cannot be. The
13
13
  // prompt, any input image, and the finished bytes pass through Privateer's servers in
14
14
  // plaintext on the way to and from the model provider — that is what generation IS.
15
15
  // What we do control: nothing is persisted server-side. The bytes come back inline and
16
16
  // land only in the file you name. Never describe a routine that generates media as
17
- // private end-to-end; do say the output isn't stored in our cloud.
17
+ // fully private; do say the output isn't stored in our cloud.
18
18
  //
19
19
  // MUSIC IS THE LOOSEST OF THESE. Neither Lyria SKU has a zero-retention endpoint and no
20
20
  // confidential music model exists, so music is deliberately exempt from the ZDR gate
@@ -16,7 +16,10 @@ const MEDIA: Record<string, string> = {
16
16
  json: "application/json", csv: "text/csv", html: "text/html", mp4: "video/mp4",
17
17
  mov: "video/quicktime", mp3: "audio/mpeg", wav: "audio/wav", zip: "application/zip",
18
18
  };
19
- function mediaTypeForPath(p: string): string {
19
+ // Exported because the inbox-attachment path (routines/resultMedia.ts) types files
20
+ // by the same table: two tables would drift, and the type decides how the app
21
+ // presents an attachment (image inline, video in a player, everything else a chip).
22
+ export function mediaTypeForPath(p: string): string {
20
23
  const ext = p.split(".").pop()?.toLowerCase() ?? "";
21
24
  return MEDIA[ext] ?? "application/octet-stream";
22
25
  }
@@ -255,7 +255,7 @@ async function opProbe(cwd: string, p: Params, signal?: AbortSignal): Promise<st
255
255
  .join("\n");
256
256
  }
257
257
 
258
- // Stitch clips end to end, optionally with a crossfade between them.
258
+ // Stitch clips back-to-back, optionally with a crossfade between them.
259
259
  async function opConcat(cwd: string, p: Params, signal?: AbortSignal): Promise<string> {
260
260
  const list = p.inputs ?? [];
261
261
  if (list.length < 2) throw new ToolError("`concat` needs at least two paths in `inputs`.");
package/src/tools/web.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  // not, but the derived query does. That is the same residual metadata leak already
17
17
  // documented for Sealed mode in the server's routes/rag.js header, and it is why web
18
18
  // access is a switch on the agent rather than an unconditional capability. Never
19
- // describe a routine that searches as private end-to-end.
19
+ // describe a routine that searches as fully private.
20
20
  //
21
21
  // SCOPE. These are the UNATTENDED paths' web tools: the harbor (routines, tasks,
22
22
  // workflow agent steps) and channels, both of which build a session from an explicit