@fastagent-sh/fastagent 0.26.1 → 0.28.0

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/README.md CHANGED
@@ -152,9 +152,9 @@ The package exports the supported surface only, from one entry per kind of user,
152
152
  | The Agent API | `Agent`, `AgentError` ([Agent API](docs/SPEC.md)) | Stable within its version |
153
153
  | Directory → agent → service | `openAgent`, `LocalAgent`, `serve()`'s `Service` (`fetch`, `listen`) | Usable now, may tighten before 1.0 |
154
154
  | Over HTTP | `connectAgent` (`/client`), the `/v1` routes ([Agent API](docs/SPEC.md) §9) | Stable within its version |
155
- | A host embedding agents | `createAgent`, `CredentialStore`, `credentialFile`, `signIn`, `signInOptions`, `providerAuth` ([embedding](docs/embedding.md#6-a-desktop-host)) | Usable now, may tighten before 1.0 |
155
+ | A host embedding agents | `createAgent`, `addContent`, `removeContent`, `listContent`, `CredentialStore`, `credentialFile`, `signIn`, `signInOptions`, `providerAuth` ([embedding](docs/embedding.md#6-a-desktop-host)) | Usable now, may tighten before 1.0 |
156
156
  | Authoring | `defineAgent`, `defineChannel`, `defineSchedule`, `needs`, `sessionIdOf`, `readBodyCapped`, `text`, `secretEquals` | Usable now, may tighten before 1.0 |
157
- | Not exported | The assembly's parts (`router`, `createScheduler`), content editing, the model environment, harness internals | No compatibility promise |
157
+ | Not exported | The assembly's parts (`router`, `createScheduler`), the environment's edit (`writeEnvironment`), the model environment, harness internals | No compatibility promise |
158
158
 
159
159
  A tool or an extension is pi-durable's own (`defineTool`, `defineExtension` from `@earendil-works/pi-durable`), and a
160
160
  custom provider is pi-ai's (`createProvider` from `@earendil-works/pi-ai`): `init` adds both packages to the agent.
@@ -52,13 +52,15 @@ declare const AgentInfo_base: Schema.Opaque<AgentInfo, Schema.Struct<{
52
52
  }>>;
53
53
  /**
54
54
  * The models a session may name: those whose provider has a credential here. `contextWindow`, in tokens, is the
55
- * window the agent runs the model with, absent when the model declares none.
55
+ * window the agent runs the model with, absent when the model declares none. `thinkingLevels` are the levels the
56
+ * model supports, lowest first: `["off"]` for a model that does not reason.
56
57
  */
57
58
  readonly models: Schema.$Array<Schema.Struct<{
58
59
  readonly provider: Schema.String;
59
60
  readonly modelId: Schema.String;
60
61
  readonly name: Schema.optionalKey<Schema.String>;
61
62
  readonly contextWindow: Schema.optionalKey<Schema.Number>;
63
+ readonly thinkingLevels: Schema.$Array<Schema.Literals<readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"]>>;
62
64
  }>>;
63
65
  readonly defaultModel: Schema.optionalKey<Schema.Struct<{
64
66
  readonly provider: Schema.String;
@@ -79,13 +81,15 @@ declare const AgentInfo_base: Schema.Opaque<AgentInfo, Schema.Struct<{
79
81
  }>>;
80
82
  /**
81
83
  * The models a session may name: those whose provider has a credential here. `contextWindow`, in tokens, is the
82
- * window the agent runs the model with, absent when the model declares none.
84
+ * window the agent runs the model with, absent when the model declares none. `thinkingLevels` are the levels the
85
+ * model supports, lowest first: `["off"]` for a model that does not reason.
83
86
  */
84
87
  readonly models: Schema.$Array<Schema.Struct<{
85
88
  readonly provider: Schema.String;
86
89
  readonly modelId: Schema.String;
87
90
  readonly name: Schema.optionalKey<Schema.String>;
88
91
  readonly contextWindow: Schema.optionalKey<Schema.Number>;
92
+ readonly thinkingLevels: Schema.$Array<Schema.Literals<readonly ["off", "minimal", "low", "medium", "high", "xhigh", "max"]>>;
89
93
  }>>;
90
94
  readonly defaultModel: Schema.optionalKey<Schema.Struct<{
91
95
  readonly provider: Schema.String;
package/dist/agent-api.js CHANGED
@@ -30,13 +30,15 @@ export class AgentInfo extends Schema.Opaque()(Schema.Struct({
30
30
  tools: Schema.Array(Named),
31
31
  /**
32
32
  * The models a session may name: those whose provider has a credential here. `contextWindow`, in tokens, is the
33
- * window the agent runs the model with, absent when the model declares none.
33
+ * window the agent runs the model with, absent when the model declares none. `thinkingLevels` are the levels the
34
+ * model supports, lowest first: `["off"]` for a model that does not reason.
34
35
  */
35
36
  models: Schema.Array(Schema.Struct({
36
37
  provider: Schema.String,
37
38
  modelId: Schema.String,
38
39
  name: Schema.optionalKey(Schema.String),
39
40
  contextWindow: Schema.optionalKey(Schema.Number),
41
+ thinkingLevels: Schema.Array(ThinkingLevel),
40
42
  })),
41
43
  defaultModel: Schema.optionalKey(ModelRef),
42
44
  })) {
@@ -10,7 +10,7 @@ export declare class ContentNameError extends Error {
10
10
  name: string;
11
11
  }
12
12
  export interface InitAgentOptions {
13
- /** What the agent works on and knows: repositories into `context.json`, this machine's directories into
13
+ /** What the agent works on: repositories into `context.json`, this machine's directories into
14
14
  * `context.local.json`. */
15
15
  content?: ContentAddition[];
16
16
  /**
@@ -36,26 +36,33 @@ export interface CreatedAgent {
36
36
  * Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
37
37
  * repository whose first commit is what was written, since the agent changes itself and version control is how its
38
38
  * author goes back to a version that worked. It installs no dependencies; the agent opens without them.
39
+ *
40
+ * `content` gives it any number of entries, each a directory of this machine or a GitHub repository. They are all
41
+ * checked before anything is written, and one that is refused leaves no directory. Two entries may bind the same
42
+ * directory, or one inside the other; the agent then reaches it by both names.
39
43
  */
40
- export declare function createAgent(dir: string): Promise<CreatedAgent>;
44
+ export declare function createAgent(dir: string, options?: {
45
+ content?: ContentAddition[];
46
+ }): Promise<CreatedAgent & {
47
+ content: ResolvedContent[];
48
+ }>;
41
49
  /**
42
- * {@link createAgent} with what `fastagent init` adds: content, and the install. Every content entry is checked before
50
+ * {@link createAgent} with what `fastagent init` adds: the install. Every content entry is checked before
43
51
  * anything is written, and the scaffold is removed again when writing the content fails.
44
52
  */
45
53
  export declare function initAgent(dir: string, options?: InitAgentOptions): Promise<CreatedAgent & {
46
54
  content: ResolvedContent[];
47
55
  }>;
48
56
  /**
49
- * The agent's content, resolved for this instance (what `fastagent content list` shows), and the directories this
50
- * machine binds that are not here: the agent is not told of those.
57
+ * The on-disk declaration resolved for this machine, not an open agent's startup snapshot. Missing local directories
58
+ * are listed separately; an agent opened here is not told of them.
51
59
  */
52
60
  export declare function listContent(agentDir: string): Promise<{
53
61
  content: ResolvedContent[];
54
62
  absent: BoundContent[];
55
63
  }>;
56
64
  /**
57
- * What an edit leaves: the entry it added or removed, by name, the agent's content after it, and what to tell the
58
- * user about what was left on this machine.
65
+ * The edited declaration resolved for this machine; an already-open agent keeps its startup snapshot until reopened.
59
66
  */
60
67
  export interface ContentEdit {
61
68
  name: string;
package/dist/authoring.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * Creating an agent and editing its content: `fastagent init` and `fastagent content` are thin wrappers over these (and
3
3
  * `fastagent env` writes its declaration through {@link writeEnvironment}), so the rules live here, not in a command.
4
4
  * Nothing here prints or exits; every refusal is thrown with the message the CLI shows. The package exports
5
- * {@link createAgent}; the content and environment edits wait for context packages, whose shape they follow.
5
+ * {@link createAgent} and the content edits ({@link addContent}, {@link removeContent}, {@link listContent}), which
6
+ * give an agent its own entries, without an alias prefix; the environment's edit is not exported.
6
7
  */
7
8
  import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
8
9
  import { rm } from "node:fs/promises";
@@ -28,13 +29,16 @@ export class ContentNameError extends Error {
28
29
  * Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
29
30
  * repository whose first commit is what was written, since the agent changes itself and version control is how its
30
31
  * author goes back to a version that worked. It installs no dependencies; the agent opens without them.
32
+ *
33
+ * `content` gives it any number of entries, each a directory of this machine or a GitHub repository. They are all
34
+ * checked before anything is written, and one that is refused leaves no directory. Two entries may bind the same
35
+ * directory, or one inside the other; the agent then reaches it by both names.
31
36
  */
32
- export async function createAgent(dir) {
33
- const { content: _, ...created } = await initAgent(dir);
34
- return created;
37
+ export async function createAgent(dir, options = {}) {
38
+ return initAgent(dir, options.content === undefined ? {} : { content: options.content });
35
39
  }
36
40
  /**
37
- * {@link createAgent} with what `fastagent init` adds: content, and the install. Every content entry is checked before
41
+ * {@link createAgent} with what `fastagent init` adds: the install. Every content entry is checked before
38
42
  * anything is written, and the scaffold is removed again when writing the content fails.
39
43
  */
40
44
  export async function initAgent(dir, options = {}) {
@@ -69,8 +73,8 @@ export async function initAgent(dir, options = {}) {
69
73
  return { dir: agentDir, created, content, repository: await initRepository(agentDir) };
70
74
  }
71
75
  /**
72
- * The agent's content, resolved for this instance (what `fastagent content list` shows), and the directories this
73
- * machine binds that are not here: the agent is not told of those.
76
+ * The on-disk declaration resolved for this machine, not an open agent's startup snapshot. Missing local directories
77
+ * are listed separately; an agent opened here is not told of them.
74
78
  */
75
79
  export async function listContent(agentDir) {
76
80
  const dir = resolveAgentDir(agentDir);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `fastagent content list|add|remove`: what the agent works on and knows, as `context.json` declares it and
2
+ * `fastagent content list|add|remove`: what the agent works on, as `context.json` declares it and
3
3
  * `context.local.json` binds it on this machine.
4
4
  */
5
5
  import { resolve } from "node:path";
@@ -8,14 +8,12 @@ import { AgentError } from "../../agent-api.js";
8
8
  import { enterAgentEnv } from "../../env.js";
9
9
  import { dotEnvPath, loadEnvValues } from "../../value-file.js";
10
10
  import { installProxyFetch } from "../../proxy.js";
11
- import { modelRefOf } from "../../harnesses/pi-durable/open.js";
12
- import { openServing } from "../../harnesses/pi-durable/serve.js";
11
+ import { modelRefOf } from "../../model-ref.js";
13
12
  import { connectAgent } from "../../http/client.js";
14
13
  import { API_KEY_VARIABLE, apiKeysFrom } from "../../http/server.js";
15
14
  import { resolveStateRoot, selectAgentEnvironment } from "../../paths.js";
16
15
  import { runInvokeJson, runInvokeStream } from "../invoke-stream.js";
17
16
  import { failStartup, failUsage, optionalAgentDirOrExit } from "../fail.js";
18
- import { enterAgentCommand, reportAuth } from "../shared.js";
19
17
  /**
20
18
  * `invoke`'s sessions, apart from the instance's storage, so `invoke` runs beside a serve of this directory, which holds
21
19
  * that one; one `invoke` at a time holds this one.
@@ -112,6 +110,10 @@ export async function runInvoke(message, dirArg, opts) {
112
110
  return runRemoteInvoke(message, dirArg, { ...opts, url: opts.url });
113
111
  if (opts.key !== undefined)
114
112
  failUsage("--key is the key --url calls with: pass --url too");
113
+ const [{ openServing }, { enterAgentCommand, reportAuth }] = await Promise.all([
114
+ import("../../harnesses/pi-durable/serve.js"),
115
+ import("../shared.js"),
116
+ ]);
115
117
  const { agentDir, modelSpec } = await enterAgentCommand(dirArg, opts);
116
118
  const file = invokeStorageFile(resolveStateRoot(agentDir));
117
119
  const { agent, models, definition } = await openServing(agentDir, {
@@ -2,7 +2,7 @@
2
2
  import type { BoundContent } from "../content/declare.ts";
3
3
  import { type ResolvedContent } from "../content/resolve.ts";
4
4
  /**
5
- * The report lines for an agent's content — `works on` / `knows`, one per entry, then a `notice` line for each
5
+ * The report lines for an agent's content — `works on`, one per entry, then a `notice` line for each
6
6
  * thing its resolution has to say, then one for each directory this machine binds that is not here — shared by the
7
7
  * startup report, `info` and `content list`, so the three say the same thing.
8
8
  */
@@ -8,13 +8,13 @@ function contentKind(c) {
8
8
  return c.clone ? `${github}, a clone brought up to date at each start` : `${github}, your checkout bound here`;
9
9
  }
10
10
  /**
11
- * The report lines for an agent's content — `works on` / `knows`, one per entry, then a `notice` line for each
11
+ * The report lines for an agent's content — `works on`, one per entry, then a `notice` line for each
12
12
  * thing its resolution has to say, then one for each directory this machine binds that is not here — shared by the
13
13
  * startup report, `info` and `content list`, so the three say the same thing.
14
14
  */
15
15
  export function contentLines(content, absent = []) {
16
16
  const lines = content.flatMap((c) => [
17
- [c.readonly ? "knows" : "works on", `${c.name.padEnd(width(content))} ${c.location} (${contentKind(c)})`],
17
+ ["works on", `${c.name.padEnd(width(content))} ${c.location} (${contentKind(c)})`],
18
18
  ...c.notices.map((notice) => ["notice", `${c.name}: ${notice}`]),
19
19
  ]);
20
20
  for (const binding of absent)
@@ -56,7 +56,7 @@ const init = {
56
56
  ],
57
57
  notes: "An agent is a directory holding a fastagent.config.ts, and it is also the agent's working directory. " +
58
58
  "It lives in a directory of its own, never inside a project or another agent: what it works on is declared " +
59
- "as its content. Add a directory it only knows later with `fastagent content add <dir> --readonly`.",
59
+ "as its content. Add a directory later with `fastagent content add <dir>`.",
60
60
  run: async (args, f) => (await import("./commands/init.js")).runInit(args[0], {
61
61
  install: f.install !== false,
62
62
  content: f.content ?? [],
@@ -434,14 +434,14 @@ const env = {
434
434
  };
435
435
  const content = {
436
436
  name: "content",
437
- summary: "list, add or remove what the agent works on and knows",
438
- description: "Content is a directory the agent works on (or only knows, with --readonly): a GitHub repository declared " +
437
+ summary: "list, add or remove what the agent works on",
438
+ description: "Content is a directory the agent works on: a GitHub repository declared " +
439
439
  "by name in context.json, shared with the agent and cloned at content/<name>, or a directory of this " +
440
440
  "machine bound in context.local.json, which never leaves it.",
441
441
  subcommands: [
442
442
  {
443
443
  name: "list",
444
- summary: "each content entry, where it is, and whether the agent works on it or only knows it",
444
+ summary: "each content entry and where it is",
445
445
  args: [AGENT_ARG],
446
446
  flags: [JSON_FLAG],
447
447
  examples: [{ cmd: "fastagent content list" }],
@@ -452,15 +452,13 @@ const content = {
452
452
  summary: "add a GitHub repository, or bind a directory of this machine, as content",
453
453
  args: [{ name: "<source>", description: "a directory, or github:owner/repo" }, AGENT_ARG],
454
454
  flags: [
455
- { flags: "--readonly", description: "the agent knows it and does not write it" },
456
455
  { flags: "--ref <ref>", description: "a repository: the branch, tag or commit to clone" },
457
456
  { flags: "--name <name>", description: "its name (default: the directory's or repository's)" },
458
457
  { flags: "--description <text>", description: "one sentence for the agent: what it is, how to treat it" },
459
458
  ],
460
459
  examples: [
461
- { cmd: "fastagent content add ~/code/app", note: "works on" },
462
- { cmd: "fastagent content add ~/handbook --readonly", note: "knows" },
463
- { cmd: "fastagent content add github:acme/docs --readonly", note: "knows a clone" },
460
+ { cmd: "fastagent content add ~/code/app", note: "a directory of this machine" },
461
+ { cmd: "fastagent content add github:acme/docs", note: "a clone" },
464
462
  ],
465
463
  notes: "The root of a GitHub checkout is declared in context.json as that repository, and bound in " +
466
464
  "context.local.json to the checkout, used as it is on this machine. github:owner/repo is cloned into " +
@@ -469,7 +467,6 @@ const content = {
469
467
  "Any other directory is bound in context.local.json only: a deployed instance works without it. A bound " +
470
468
  "directory may not contain the agent directory, nor sit inside it.",
471
469
  run: async (args, f) => (await import("./commands/content.js")).runContentAdd(args[0], args[1], {
472
- readonly: f.readonly === true,
473
470
  ...(typeof f.ref === "string" ? { ref: f.ref } : {}),
474
471
  ...(typeof f.name === "string" ? { name: f.name } : {}),
475
472
  ...(typeof f.description === "string" ? { description: f.description } : {}),
@@ -4,8 +4,6 @@ export interface ContentEntry {
4
4
  github: string;
5
5
  /** The branch, tag or commit to clone. */
6
6
  ref?: string;
7
- /** The agent knows it and does not write it. */
8
- readonly?: boolean;
9
7
  /** One sentence for the agent: what this is, and how to treat it. */
10
8
  description?: string;
11
9
  }
@@ -15,7 +13,6 @@ export interface DeclaredContent {
15
13
  /** `owner/repo`. */
16
14
  repo: string;
17
15
  ref?: string;
18
- readonly: boolean;
19
16
  description?: string;
20
17
  }
21
18
  /**
@@ -25,7 +22,6 @@ export interface DeclaredContent {
25
22
  export interface ContentBinding {
26
23
  /** Absolute, `~/`, or relative to the agent directory. */
27
24
  path: string;
28
- readonly?: boolean;
29
25
  description?: string;
30
26
  }
31
27
  /** A binding, read: `path` absolute. */
@@ -38,7 +34,6 @@ export type BoundContent = {
38
34
  } | {
39
35
  /** A directory only this machine has. */
40
36
  kind: "local";
41
- readonly: boolean;
42
37
  description?: string;
43
38
  });
44
39
  /** What an agent's content is declared as: the shared declaration, and this machine's bindings. */
@@ -64,7 +59,7 @@ export declare function declareContent(raw: unknown): DeclaredContent[];
64
59
  export declare function declareBindings(raw: unknown, declared: readonly DeclaredContent[], agentDir: string): BoundContent[];
65
60
  /**
66
61
  * A directory the agent is bound to and the agent directory are kept apart (agent-model.md §2): a definition is
67
- * released, writable content is kept, and one directory cannot be both. The ONE statement of the rule; resolve.ts asks
62
+ * released, content is kept, and one directory cannot be both. The ONE statement of the rule; resolve.ts asks
68
63
  * it of the real paths.
69
64
  */
70
65
  export declare function nestingError(agentDir: string, location: string, name: string): string | undefined;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * WHAT AN AUTHOR DECLARES an agent works on or knows, read and refused in ONE place (file.ts reads the files through
2
+ * WHAT AN AUTHOR DECLARES an agent works on, read and refused in ONE place (file.ts reads the files through
3
3
  * it): the `content` map of `context.json`, which is shared and so holds only repositories every instance reaches, and
4
4
  * the `content` map of `context.local.json`, the directories of this machine it is bound to. Pure: what is on the disk
5
5
  * is resolve.ts's question. docs/design/agent-model.md §3 is the rule.
@@ -11,8 +11,8 @@ import { CONTEXT_FILE, LOCAL_CONTEXT_FILE, isUnderDir } from "../paths.js";
11
11
  * The keys an entry and a binding carry, in the order they are written: where it is, then how it is treated. One list
12
12
  * each, so a key the reader accepts is one the writer keeps.
13
13
  */
14
- const ENTRY_KEYS = ["github", "ref", "readonly", "description"];
15
- const BINDING_KEYS = ["path", "readonly", "description"];
14
+ const ENTRY_KEYS = ["github", "ref", "description"];
15
+ const BINDING_KEYS = ["path", "description"];
16
16
  /** A name becomes a directory name, so it is one path segment (the spelling a release's agent name has). */
17
17
  const NAME = /^[A-Za-z0-9_-]+$/;
18
18
  /** Whether `name` can name a content entry. */
@@ -62,16 +62,14 @@ export function declareBindings(raw, declared, agentDir) {
62
62
  if (shared.name !== name) {
63
63
  throw new Error(`${at} binds the entry ${CONTEXT_FILE} names "${shared.name}": spell it the same`);
64
64
  }
65
- for (const key of ["readonly", "description"]) {
66
- if (e[key] !== undefined) {
67
- throw new Error(`${at} binds this machine's checkout of the entry ${CONTEXT_FILE} declares, which says "${key}" for ` +
68
- `every instance: give it only "path"`);
69
- }
65
+ if (e.description !== undefined) {
66
+ throw new Error(`${at} binds this machine's checkout of the entry ${CONTEXT_FILE} declares, which says "description" for ` +
67
+ `every instance: give it only "path"`);
70
68
  }
71
69
  return { name, path, kind: "checkout" };
72
70
  }
73
- const treatment = treatmentOf(at, e);
74
- return { name, path, kind: "local", ...treatment };
71
+ const description = descriptionOf(at, e);
72
+ return { name, path, kind: "local", ...(description !== undefined ? { description } : {}) };
75
73
  });
76
74
  }
77
75
  /** `path` as written in `context.local.json`, absolute: `~/` is the home directory, a relative one the agent's. */
@@ -112,14 +110,11 @@ function objectWithKeys(at, value, keys) {
112
110
  }
113
111
  return e;
114
112
  }
115
- function treatmentOf(at, e) {
113
+ function descriptionOf(at, e) {
116
114
  if (e.description !== undefined && (typeof e.description !== "string" || e.description === "")) {
117
115
  throw new Error(`${at}: "description" must be a non-empty string`);
118
116
  }
119
- if (e.readonly !== undefined && typeof e.readonly !== "boolean")
120
- throw new Error(`${at}: "readonly" must be a boolean`);
121
- const description = e.description;
122
- return { readonly: e.readonly === true, ...(description !== undefined ? { description } : {}) };
117
+ return e.description;
123
118
  }
124
119
  function declareOne(name, entry) {
125
120
  const at = `content "${name}"`;
@@ -129,7 +124,7 @@ function declareOne(name, entry) {
129
124
  throw new Error(`${at}: "${key}" must be a non-empty string`);
130
125
  }
131
126
  }
132
- const treatment = treatmentOf(at, e);
127
+ const description = descriptionOf(at, e);
133
128
  if (e.github === undefined) {
134
129
  throw new Error(`${at} needs "github": ${CONTEXT_FILE} is shared, so it declares only what every instance reaches — bind a ` +
135
130
  `directory of this machine in ${LOCAL_CONTEXT_FILE} instead (fastagent content add <dir>)`);
@@ -141,11 +136,11 @@ function declareOne(name, entry) {
141
136
  // git reads a leading "-" as an option, and no branch, tag or commit name has one.
142
137
  if (ref?.startsWith("-"))
143
138
  throw new Error(`${at}: "ref" must name a branch, tag or commit, got "${ref}"`);
144
- return { name, repo, ...(ref !== undefined ? { ref } : {}), ...treatment };
139
+ return { name, repo, ...(ref !== undefined ? { ref } : {}), ...(description !== undefined ? { description } : {}) };
145
140
  }
146
141
  /**
147
142
  * A directory the agent is bound to and the agent directory are kept apart (agent-model.md §2): a definition is
148
- * released, writable content is kept, and one directory cannot be both. The ONE statement of the rule; resolve.ts asks
143
+ * released, content is kept, and one directory cannot be both. The ONE statement of the rule; resolve.ts asks
149
144
  * it of the real paths.
150
145
  */
151
146
  export function nestingError(agentDir, location, name) {
@@ -4,8 +4,6 @@ import { type CloneOutcome } from "./git.ts";
4
4
  export type ResolvedContent = {
5
5
  /** Unique within the agent, ignoring case; one path segment. */
6
6
  name: string;
7
- /** The agent knows it and does not write it. */
8
- readonly: boolean;
9
7
  /** What the declaration tells the agent about it. */
10
8
  description?: string;
11
9
  /** Where the agent reaches it on this instance, absolute. */
@@ -33,10 +33,9 @@ export function absentNotice(binding) {
33
33
  `not told of it`);
34
34
  }
35
35
  function resolveRepository(agentDir, entry, checkout) {
36
- const { name, readonly, description } = entry;
36
+ const { name, description } = entry;
37
37
  const repository = {
38
38
  name,
39
- readonly,
40
39
  ...(description !== undefined ? { description } : {}),
41
40
  kind: "github",
42
41
  repo: entry.repo,
@@ -62,12 +61,11 @@ function resolveRepository(agentDir, entry, checkout) {
62
61
  return { ...repository, location, notices, clone: true };
63
62
  }
64
63
  function resolveLocal(agentDir, binding) {
65
- const { name, readonly, description } = binding;
64
+ const { name, description } = binding;
66
65
  const location = realpathSync(binding.path);
67
66
  checkBinding(agentDir, name, location);
68
67
  return {
69
68
  name,
70
- readonly,
71
69
  ...(description !== undefined ? { description } : {}),
72
70
  location,
73
71
  notices: [],
@@ -1,6 +1,5 @@
1
1
  import { type ContentBinding, type ContentEntry } from "./declare.ts";
2
2
  export interface SourceOptions {
3
- readonly?: boolean;
4
3
  name?: string;
5
4
  /** A github entry's branch, tag or commit. */
6
5
  ref?: string;
@@ -14,7 +14,6 @@ import { checkoutOf, githubRepoOf } from "./git.js";
14
14
  */
15
15
  export function readContentSource(source, cwd, options = {}) {
16
16
  const treated = {
17
- ...(options.readonly ? { readonly: true } : {}),
18
17
  ...(options.description !== undefined ? { description: options.description } : {}),
19
18
  };
20
19
  const ref = options.ref !== undefined ? { ref: options.ref } : {};
@@ -27,33 +27,25 @@ import { checkPackages, MISE_PACKAGE_NAMES, miseBinary, packageSpecs } from "../
27
27
  /**
28
28
  * What each content entry becomes on the host, one line each, so none is missing there unsaid (agent-model.md §3). A
29
29
  * repository `context.json` declares is cloned there, as on this machine without a checkout bound. A directory only
30
- * this machine binds stays here: the deployed agent works without it, which the line says and says how to change. One
31
- * the agent works on is a warning, since the deployed agent lacks data it was meant to work on; one it only knows, a
32
- * note.
30
+ * this machine binds stays here: the deployed agent works without it, which the line says and says how to change. It
31
+ * is a warning, since the deployed agent lacks data it was meant to work on.
33
32
  */
34
33
  function checkContent({ declared, bound }, storageResets, report) {
35
34
  for (const entry of declared) {
36
35
  const fate = storageResets
37
36
  ? "cloned afresh on every deployment, since the host's storage starts over; what the agent did not push is lost"
38
37
  : "cloned on the host, and brought up to date in place at each start";
39
- report.note(`${roleOf(entry)} ${entry.name}: github ${entry.repo}${entry.ref ? `@${entry.ref}` : ""}, ${fate}`);
38
+ report.note(`works on ${entry.name}: github ${entry.repo}${entry.ref ? `@${entry.ref}` : ""}, ${fate}`);
40
39
  }
41
40
  for (const binding of bound) {
42
41
  if (binding.kind !== "local")
43
42
  continue;
44
- const stays = `${roleOf(binding)} ${binding.name}: a directory of this machine (${binding.path}, bound in ` +
43
+ const stays = `works on ${binding.name}: a directory of this machine (${binding.path}, bound in ` +
45
44
  `${LOCAL_CONTEXT_FILE}), stays here, and the deployed agent works without it`;
46
- if (binding.readonly) {
47
- report.note(`${stays}; to ship what the agent reads there, copy it into the agent directory, which every release carries`);
48
- }
49
- else {
50
- report.warn(`${stays}; to work on it from a host, move it to a GitHub repository and add it as github:owner/repo`);
51
- }
45
+ report.warn(`${stays}; to work on it from a host, move it to a GitHub repository and add it as github:owner/repo, or copy ` +
46
+ `what it only needs to read there into the agent directory, which every release carries`);
52
47
  }
53
48
  }
54
- function roleOf(entry) {
55
- return entry.readonly ? "knows" : "works on";
56
- }
57
49
  /**
58
50
  * What the image needs to install the packages the agent declares: the agent's own mise for linux in its package.json,
59
51
  * so the image's install brings it, packages FastAgent installs from, and a PyPI package's runtime. A machine without
@@ -1,4 +1,4 @@
1
- import type { Models, ModelThinkingLevel } from "@earendil-works/pi-ai";
1
+ import { type Models, type ModelThinkingLevel } from "@earendil-works/pi-ai";
2
2
  import { type Conversation, Harness, type HarnessOptions, type HarnessSettings, type ModelRef, type RegistryReader, type Storage } from "@earendil-works/pi-durable";
3
3
  import { type Agent } from "../../agent-api.ts";
4
4
  /** What a Caller may offer as `/<name>`: a prompt template or a skill, expanded for the model (SPEC.md §5.1). */
@@ -4,6 +4,7 @@
4
4
  * one process owns a storage.
5
5
  */
6
6
  import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
7
+ import { getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
7
8
  import { AgentDoc, ConversationBusy, DEFAULT_RETRY_POLICY, Harness, InboxDoc, LiveDoc, SessionFailed, configure, } from "@earendil-works/pi-durable";
8
9
  import * as Cause from "effect/Cause";
9
10
  import * as Duration from "effect/Duration";
@@ -181,6 +182,7 @@ function durableAgent(harness, ids, sessions, lifetime, options) {
181
182
  ...(model.name ? { name: model.name } : {}),
182
183
  // pi reads a window a model does not declare (a virtual model's, before its first answer) as 0.
183
184
  ...(model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
185
+ thinkingLevels: getSupportedThinkingLevels(model),
184
186
  })),
185
187
  ...(options.model === undefined
186
188
  ? {}
@@ -1,5 +1,5 @@
1
1
  import type { ModelThinkingLevel, Models, Provider } from "@earendil-works/pi-ai";
2
- import { type Extension, type HarnessSettings, type ModelRef, type Storage, type ToolRegistration } from "@earendil-works/pi-durable";
2
+ import { type Extension, type HarnessSettings, type Storage, type ToolRegistration } from "@earendil-works/pi-durable";
3
3
  import type { ResolvedContent } from "../../content/resolve.ts";
4
4
  import { type DeclaredSecret } from "../../declared-secrets.ts";
5
5
  import type { ChannelModule, LongConnectionChannelModule } from "../../channel.ts";
@@ -14,7 +14,6 @@ export interface AgentDefinition {
14
14
  config: AgentConfig;
15
15
  registry: Registry;
16
16
  }
17
- export declare function modelRefOf(spec: string, where: string): ModelRef;
18
17
  /** The agent package's `fastagent.config.ts`, checked; `{}` without one. */
19
18
  export declare function loadConfig(dir: string): Promise<AgentConfig>;
20
19
  /** The default model a process here runs with: `--model` > `FASTAGENT_MODEL` > the configuration's `model`. */
@@ -65,7 +64,7 @@ export declare function buildDefinitionShape(definition: AgentDefinition, env: N
65
64
  export declare function definitionProviders(agentDir: string, env?: NodeJS.ProcessEnv): Promise<Provider[]>;
66
65
  export interface AssembleAgentOptions {
67
66
  storage: Storage;
68
- /** What the agent works on and knows (`context.json`'s content), resolved for this instance. */
67
+ /** What the agent works on (`context.json`'s content), resolved for this instance. */
69
68
  content?: readonly ResolvedContent[];
70
69
  /** The definition's code, built once with its secrets (`buildDefinition`). */
71
70
  code: BuiltDefinition;
@@ -25,6 +25,7 @@ import { channelModuleProblem } from "../../channels/discover.js";
25
25
  import { AGENT_CONFIG_FILE as CONFIG_FILE } from "../../paths.js";
26
26
  import { readPackageJson } from "../../runtime.js";
27
27
  import { DEV_SERVE_ENV } from "../../serving-command.js";
28
+ import { modelRefOf } from "../../model-ref.js";
28
29
  import { MAX_SCHEDULES, scheduleConfigProblem } from "../../schedule/schedule.js";
29
30
  import { openDurableAgent } from "./agent.js";
30
31
  import { isNeeds } from "./define.js";
@@ -97,13 +98,6 @@ function checkConfig(config, file) {
97
98
  }
98
99
  return config;
99
100
  }
100
- export function modelRefOf(spec, where) {
101
- const slash = typeof spec === "string" ? spec.indexOf("/") : -1;
102
- if (slash < 1 || slash === spec.length - 1) {
103
- throw new Error(`${where} must be "provider/modelId", got ${JSON.stringify(spec)}`);
104
- }
105
- return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
106
- }
107
101
  /** The agent package's `fastagent.config.ts`, checked; `{}` without one. */
108
102
  export async function loadConfig(dir) {
109
103
  const configPath = join(dir, CONFIG_FILE);
@@ -39,7 +39,7 @@ export interface OpenedServing {
39
39
  definition: AgentDefinition;
40
40
  code: BuiltDefinition;
41
41
  models: ModelEnvironment;
42
- /** What the agent works on and knows, resolved for this instance. */
42
+ /** What the agent works on, resolved for this instance. */
43
43
  content: readonly ResolvedContent[];
44
44
  agentDir: string;
45
45
  /** Where the instance's state is: its storage, its channels' and schedules' state, its wake-ups. */
@@ -263,14 +263,12 @@ function contentKind(c) {
263
263
  // changes stays, and reaches anyone else only when pushed. The declared ref is what it follows, not a claim about
264
264
  // where it is: a clone the agent moved elsewhere is left there.
265
265
  const clone = `a shallow clone of github ${c.repo}${c.ref ? ` (declared ${c.ref})` : ""} in your own storage`;
266
- return c.readonly
267
- ? `${clone}, brought up to date each time you start`
268
- : `${clone}: what you change in it stays, and each time you start it is brought up to date where that touches nothing of yours; push to share a change`;
266
+ return `${clone}: what you change in it stays, and each time you start it is brought up to date where that touches nothing of yours; push to share a change`;
269
267
  }
270
268
  /**
271
269
  * Where the agent is and what it works on (agent-model.md §4): its own directory is itself, and each content entry is named
272
- * with its location, whether it is the agent's to change, and where a change there goes. A result written where it
273
- * does not last is how knowledge gets lost, so the agent is told which is which.
270
+ * with its location and where a change there goes. A result written where it does not last is how knowledge gets
271
+ * lost, so the agent is told which is which.
274
272
  */
275
273
  function contentSection({ agentDir, content }) {
276
274
  const own = `Your working directory, ${agentDir}, is your own directory: it holds your definition, and files you create ` +
@@ -278,15 +276,11 @@ function contentSection({ agentDir, content }) {
278
276
  if (content.length === 0)
279
277
  return `${own} Nothing else is declared for you: you work only in your own directory.`;
280
278
  const line = (c) => `- ${c.name}: ${c.location} (${contentKind(c)})${c.description !== undefined ? ` ${c.description}` : ""}`;
281
- const worksOn = content.filter((c) => !c.readonly);
282
- const knows = content.filter((c) => c.readonly);
283
279
  return [
284
280
  own,
285
- ...(worksOn.length > 0 ? ["You work on:", ...worksOn.map(line)] : []),
286
- ...(knows.length > 0 ? ["You know, and do not write:", ...knows.map(line)] : []),
287
- `Run a command in one of these directories with \`cd <its location> && …\`; file tools take the location directly.` +
288
- (worksOn.length > 0
289
- ? " A result worth keeping belongs in a directory you work on, not in your own directory."
290
- : " You write only in your own directory: the directories above are for reading."),
281
+ "You work on:",
282
+ ...content.map(line),
283
+ `Run a command in one of these directories with \`cd <its location> && …\`; file tools take the location directly. ` +
284
+ `A result worth keeping belongs in a directory you work on, not in your own directory.`,
291
285
  ].join("\n");
292
286
  }
package/dist/index.d.ts CHANGED
@@ -7,7 +7,11 @@ export type { Agent, AgentErrorData, AgentInfo, CompactOptions, ContentPart, Cre
7
7
  export { AgentError, LIMITS, hasEnded } from "./agent-api.ts";
8
8
  export { openAgent, type LocalAgent, type OpenAgentOptions, type ServeAgentOptions, } from "./harnesses/pi-durable/serve.ts";
9
9
  export type { Service } from "./service.ts";
10
- export { createAgent, type CreatedAgent } from "./authoring.ts";
10
+ export { ContentNameError, addContent, createAgent, listContent, removeContent, type ContentEdit, type CreatedAgent, } from "./authoring.ts";
11
+ export type { ContentAddition, SourceOptions } from "./content/source.ts";
12
+ export { readContentSource } from "./content/source.ts";
13
+ export type { BoundContent, ContentBinding, ContentEntry } from "./content/declare.ts";
14
+ export type { ResolvedContent } from "./content/resolve.ts";
11
15
  export { SignInCancelled, type Credential, type CredentialStore, type ProviderAuth, type SignInEvent, type SignInInteraction, type SignInMethod, type SignInOption, type SignInPrompt, type SignInRequest, type SignInResult, } from "./sign-in-api.ts";
12
16
  export { credentialFile, providerAuth, signIn, signInOptions } from "./harnesses/models/host.ts";
13
17
  export { defineAgent, defineSchedule, needs, type AgentConfig, type Needs, type SecretSpec, type SecretValues, } from "./harnesses/pi-durable/define.ts";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { AgentError, LIMITS, hasEnded } from "./agent-api.js";
2
2
  export { openAgent, } from "./harnesses/pi-durable/serve.js";
3
- export { createAgent } from "./authoring.js";
3
+ export { ContentNameError, addContent, createAgent, listContent, removeContent, } from "./authoring.js";
4
+ export { readContentSource } from "./content/source.js";
4
5
  export { SignInCancelled, } from "./sign-in-api.js";
5
6
  export { credentialFile, providerAuth, signIn, signInOptions } from "./harnesses/models/host.js";
6
7
  export { defineAgent, defineSchedule, needs, } from "./harnesses/pi-durable/define.js";
@@ -0,0 +1,3 @@
1
+ /** Model selection syntax shared by a local definition and a remote Caller, without loading a harness. */
2
+ import type { ModelRef } from "./agent-api.ts";
3
+ export declare function modelRefOf(spec: string, where: string): ModelRef;
@@ -0,0 +1,7 @@
1
+ export function modelRefOf(spec, where) {
2
+ const slash = typeof spec === "string" ? spec.indexOf("/") : -1;
3
+ if (slash < 1 || slash === spec.length - 1) {
4
+ throw new Error(`${where} must be "provider/modelId", got ${JSON.stringify(spec)}`);
5
+ }
6
+ return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastagent-sh/fastagent",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Vibe first. Then FastAgent: turn a local agent directory into a live service in your app, on Telegram, Slack, Feishu, or behind any channel.",
5
5
  "keywords": [
6
6
  "agent",