@parcha/agentrun-pi 0.1.0-beta.2 → 0.1.0-beta.20

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.
Files changed (114) hide show
  1. package/README.md +104 -27
  2. package/dist/cli.js +15 -4
  3. package/dist/demo-cli.d.ts +2 -0
  4. package/dist/demo-cli.js +25 -0
  5. package/dist/demo.js +1 -0
  6. package/dist/durable/decision-recovery.d.ts +23 -0
  7. package/dist/durable/decision-recovery.js +57 -0
  8. package/dist/durable/decisions.d.ts +113 -0
  9. package/dist/durable/decisions.js +71 -0
  10. package/dist/durable/index.d.ts +20 -0
  11. package/dist/durable/index.js +14 -0
  12. package/dist/durable/node-runner.d.ts +106 -0
  13. package/dist/durable/node-runner.js +253 -0
  14. package/dist/durable/node.d.ts +88 -0
  15. package/dist/durable/node.js +98 -0
  16. package/dist/durable/pi-versions.d.ts +34 -0
  17. package/dist/durable/pi-versions.js +66 -0
  18. package/dist/durable/readers.d.ts +28 -0
  19. package/dist/durable/readers.js +103 -0
  20. package/dist/durable/receipts.d.ts +54 -0
  21. package/dist/durable/receipts.js +92 -0
  22. package/dist/durable/record-file.d.ts +10 -0
  23. package/dist/durable/record-file.js +56 -0
  24. package/dist/durable/record-tool.d.ts +76 -0
  25. package/dist/durable/record-tool.js +123 -0
  26. package/dist/durable/record.d.ts +157 -0
  27. package/dist/durable/record.js +402 -0
  28. package/dist/durable/recording.d.ts +90 -0
  29. package/dist/durable/recording.js +292 -0
  30. package/dist/durable/store.d.ts +23 -0
  31. package/dist/durable/store.js +240 -0
  32. package/dist/durable/task.d.ts +133 -0
  33. package/dist/durable/task.js +411 -0
  34. package/dist/durable/workflow-tool.d.ts +32 -0
  35. package/dist/durable/workflow-tool.js +188 -0
  36. package/dist/extension-service.js +63 -22
  37. package/dist/extension.d.ts +2 -0
  38. package/dist/extension.js +469 -80
  39. package/dist/host-addendum.d.ts +4 -0
  40. package/dist/host-addendum.js +20 -0
  41. package/dist/host-session.js +5 -2
  42. package/dist/index.d.ts +1 -2
  43. package/dist/index.js +1 -2
  44. package/dist/pi-host.d.ts +7 -0
  45. package/dist/pi-host.js +22 -0
  46. package/dist/presentation.js +2 -2
  47. package/dist/run-error.d.ts +6 -0
  48. package/dist/run-error.js +13 -0
  49. package/dist/runner.d.ts +1 -6
  50. package/dist/runner.js +7 -13
  51. package/dist/signin/index.d.ts +9 -0
  52. package/dist/signin/index.js +7 -0
  53. package/dist/signin/limit.d.ts +10 -0
  54. package/dist/signin/limit.js +23 -0
  55. package/dist/signin/resolve.d.ts +47 -0
  56. package/dist/signin/resolve.js +163 -0
  57. package/dist/signin/signin.d.ts +44 -0
  58. package/dist/signin/signin.js +126 -0
  59. package/dist/signin/spec.d.ts +116 -0
  60. package/dist/signin/spec.js +181 -0
  61. package/dist/signin/store.d.ts +40 -0
  62. package/dist/signin/store.js +206 -0
  63. package/dist/skills/author/SKILL.md +138 -0
  64. package/{skills → dist/skills}/author/references/jev-decisions.md +27 -14
  65. package/dist/skills/author/references/language.md +134 -0
  66. package/dist/skills/author/references/sdk-authoring.md +11 -0
  67. package/{skills → dist/skills}/author/references/workflow-format.md +7 -15
  68. package/dist/sop.d.ts +12 -0
  69. package/dist/sop.js +29 -0
  70. package/dist/thinking.d.ts +3 -0
  71. package/dist/thinking.js +7 -0
  72. package/dist/tool-context.d.ts +21 -0
  73. package/dist/tool-context.js +13 -0
  74. package/dist/triage-demo.d.ts +62 -0
  75. package/dist/triage-demo.js +153 -0
  76. package/dist/types.d.ts +7 -0
  77. package/dist/workflow-inspector.d.ts +47 -0
  78. package/dist/workflow-inspector.js +235 -0
  79. package/dist/workflow-session.d.ts +23 -0
  80. package/dist/workflow-session.js +168 -0
  81. package/dist/workflow-store.d.ts +36 -0
  82. package/dist/workflow-store.js +255 -0
  83. package/dist/workflow-tool.d.ts +81 -0
  84. package/dist/workflow-tool.js +94 -0
  85. package/dist/workflow-view.d.ts +64 -0
  86. package/dist/workflow-view.js +273 -0
  87. package/dist/workflows.d.ts +1 -0
  88. package/dist/workflows.js +3 -0
  89. package/package.json +57 -11
  90. package/dist/admission.d.ts +0 -6
  91. package/dist/admission.js +0 -51
  92. package/dist/author.d.ts +0 -21
  93. package/dist/author.js +0 -72
  94. package/dist/benchmark-contract.d.ts +0 -88
  95. package/dist/benchmark-contract.js +0 -50
  96. package/dist/benchmark-freeze.d.ts +0 -9
  97. package/dist/benchmark-freeze.js +0 -9
  98. package/dist/benchmark.d.ts +0 -127
  99. package/dist/benchmark.js +0 -260
  100. package/dist/comparison.d.ts +0 -69
  101. package/dist/comparison.js +0 -229
  102. package/dist/skill-bundle.d.ts +0 -5
  103. package/dist/skill-bundle.js +0 -15
  104. package/skills/author/SKILL.md +0 -102
  105. package/skills/author/references/sdk-authoring.md +0 -16
  106. /package/{skills → dist/skills}/author/examples/evidence-gate.input.json +0 -0
  107. /package/{skills → dist/skills}/author/examples/evidence-gate.json +0 -0
  108. /package/{skills → dist/skills}/author/examples/read-file.input.json +0 -0
  109. /package/{skills → dist/skills}/author/examples/read-file.json +0 -0
  110. /package/{skills → dist/skills}/author/examples/read-source-decision.input.json +0 -0
  111. /package/{skills → dist/skills}/author/examples/read-source-decision.json +0 -0
  112. /package/{skills → dist/skills}/author/examples/release-note.txt +0 -0
  113. /package/{skills → dist/skills}/author/examples/review-release-notes.input.json +0 -0
  114. /package/{skills → dist/skills}/author/examples/review-release-notes.json +0 -0
@@ -0,0 +1,4 @@
1
+ import { type AuthorHostAddendum } from '@parcha/agentrun-dsl';
2
+ /** What this extension adds to the shared workflow language: its commands, tools and unavailable transports.
3
+ * The packaged skill is rendered with it, and `describe` returns it as `authoring.host`. */
4
+ export declare const PI_HOST_ADDENDUM: AuthorHostAddendum;
@@ -0,0 +1,20 @@
1
+ import { WORKFLOW_NODE_KINDS } from '@parcha/agentrun-dsl';
2
+ /** What this extension adds to the shared workflow language: its commands, tools and unavailable transports.
3
+ * The packaged skill is rendered with it, and `describe` returns it as `authoring.host`. */
4
+ export const PI_HOST_ADDENDUM = Object.freeze({
5
+ name: 'Pi extension',
6
+ nodeKinds: WORKFLOW_NODE_KINDS.filter(kind => kind !== 'artifact'),
7
+ rules: [
8
+ 'Call agentrun with action describe first, then inspect the complete workflow with its input, then dry-run it (action dry-run, without a workflow argument), then run without a workflow argument.',
9
+ 'A dry run runs the inspected workflow on synthetic values, or the input you give it, with no model, Jev or tool call. Its status is passed, not_run (a check could not run; skipped says why, and it proves nothing) or failed (problems say why). Report not_run as not run, never as passed.',
10
+ 'Use exactly the tools describe returns. Without host-configured tools these are the active read-only built-ins (read, grep, find, ls), and bash, edit and write exist only in a trusted run; a host that configures its tools offers only those, including in trusted runs. The search tool reads fictional demo sources only.',
11
+ 'Code nodes and mutating tools run only after the user issues /agentrun run --trusted for that run. Trusted execution is local and unsandboxed. A dry run of a workflow with code nodes is not_run until the user issues /agentrun dry-run --trusted.',
12
+ 'A call uses via tool with a tool describe returns. Shell and executor calls and artifact delivery are unavailable.',
13
+ 'This extension supplies no SOP text: do not run a workflow with sopSection here; it needs an SDK host that supplies the authoritative SOP.',
14
+ 'Custom extension tools and outer permission hooks are not inherited, including in trusted runs.',
15
+ 'A preview, procedure edit or input change does not authorize execution: inspect it, report Inspected — not run, and execute only when requested.',
16
+ 'Use /agentrun to inspect the current graph, /agentrun input to change input, /agentrun save and /agentrun load for named definition revisions, /agentrun history for retained receipts, /agentrun run to restart, and /agentrun stop to request cancellation; closing the inspector does not stop a run.',
17
+ 'Saving retains the definition without input or execution permission; loading rechecks current host capabilities and never resumes an interrupted run.',
18
+ 'The structural limits describe returns bound node count, map concurrency and parallel branches.',
19
+ ],
20
+ });
@@ -1,5 +1,6 @@
1
1
  import { Agent } from '@earendil-works/pi-agent-core';
2
2
  import { createPiRunner } from './runner.js';
3
+ import { workflowToolContext } from './tool-context.js';
3
4
  export const PI_MODEL_SETUP_MESSAGE = 'AgentRun requires an active Pi model. Use /login to connect a provider, then /model to select it. Try /agentrun demo without model access.';
4
5
  function selectedModel(context) {
5
6
  const model = context.model;
@@ -16,6 +17,7 @@ function selectedModel(context) {
16
17
  export function createPiHostSessionFactory(context) {
17
18
  const model = selectedModel(context);
18
19
  const stream = context.modelRegistry.streamSimple.bind(context.modelRegistry);
20
+ const toolContext = workflowToolContext(context);
19
21
  return async (options) => {
20
22
  if (options.model !== model)
21
23
  throw new Error('The child session must use the captured active Pi model');
@@ -25,7 +27,7 @@ export function createPiHostSessionFactory(context) {
25
27
  name: tool.name, label: tool.label, description: tool.description,
26
28
  parameters: tool.parameters,
27
29
  async execute(id, args, signal, onUpdate) {
28
- const result = await tool.execute(id, args, signal, onUpdate, context);
30
+ const result = await tool.execute(id, args, signal, onUpdate, toolContext);
29
31
  if (tool.name === 'submit' && result.details?.accepted === true)
30
32
  delivered = true;
31
33
  return result;
@@ -72,9 +74,10 @@ export function createPiHostSessionFactory(context) {
72
74
  }
73
75
  export function createPiHostRunner(context, options = {}) {
74
76
  const model = selectedModel(context);
77
+ // The host session's thinking level is not a node's: a node that names none thinks at medium, unless the caller
78
+ // passes its own `thinkingLevel`.
75
79
  return createPiRunner({
76
80
  ...options, model, cwd: options.cwd ?? context.cwd,
77
- thinkingLevel: options.thinkingLevel ?? context.thinkingLevel,
78
81
  sessionFactory: createPiHostSessionFactory(context),
79
82
  });
80
83
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from "./runner.js";
2
- export * from "./author.js";
3
2
  export * from "./types.js";
4
3
  export { createPiHostRunner, createPiHostSessionFactory } from './host-session.js';
5
4
  export { WorkflowExtensionService } from './extension-service.js';
6
- export { loadPiAuthorSkillBundle } from './skill-bundle.js';
5
+ export { PI_HOST_ADDENDUM } from './host-addendum.js';
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from "./runner.js";
2
- export * from "./author.js";
3
2
  export * from "./types.js";
4
3
  export { createPiHostRunner, createPiHostSessionFactory } from './host-session.js';
5
4
  export { WorkflowExtensionService } from './extension-service.js';
6
- export { loadPiAuthorSkillBundle } from './skill-bundle.js';
5
+ export { PI_HOST_ADDENDUM } from './host-addendum.js';
@@ -0,0 +1,7 @@
1
+ export declare const SUPPORTED_PI_RANGE: string;
2
+ export declare const NEWEST_TESTED_PI_VERSION = "1.3.0";
3
+ /** Whether Pi `version` is in the supported range. As npm reads the range, a prerelease is not; nor is a version that
4
+ * is not `x.y.z`. */
5
+ export declare function isSupportedPi(version: string): boolean;
6
+ /** The status line for Pi host `version`: the supported range, and for a host outside it the newest tested one. */
7
+ export declare function piHostLine(version: string): string;
@@ -0,0 +1,22 @@
1
+ // The Pi hosts this release supports: the range of its pi-durable and chord peers. `/agentrun status` flags a host
2
+ // outside it, without blocking it, and names the newest version CI runs (the `pi-1-2` job) as the one to install.
3
+ const FLOOR = [1, 0, 4];
4
+ const CEILING = [1, 4, 0];
5
+ export const SUPPORTED_PI_RANGE = `>=${FLOOR.join(".")} <${CEILING.join(".")}`;
6
+ export const NEWEST_TESTED_PI_VERSION = "1.3.0";
7
+ const compare = (a, b) => (a[0] - b[0]) || (a[1] - b[1]) || (a[2] - b[2]);
8
+ /** Whether Pi `version` is in the supported range. As npm reads the range, a prerelease is not; nor is a version that
9
+ * is not `x.y.z`. */
10
+ export function isSupportedPi(version) {
11
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
12
+ if (!match)
13
+ return false;
14
+ const parsed = match.slice(1).map(Number);
15
+ return compare(parsed, FLOOR) >= 0 && compare(parsed, CEILING) < 0;
16
+ }
17
+ /** The status line for Pi host `version`: the supported range, and for a host outside it the newest tested one. */
18
+ export function piHostLine(version) {
19
+ return isSupportedPi(version)
20
+ ? `Pi host: ${version} (supported: ${SUPPORTED_PI_RANGE})`
21
+ : `Pi host: ${version} (outside the supported range ${SUPPORTED_PI_RANGE}; the newest tested host: npm install -g @earendil-works/pi-coding-agent@${NEWEST_TESTED_PI_VERSION})`;
22
+ }
@@ -12,7 +12,7 @@ function lines(value, depth = 0) {
12
12
  }).concat(value.length > 30 ? [`… ${value.length - 30} more items in the structured result`] : []);
13
13
  const entries = Object.entries(value);
14
14
  return entries.slice(0, 30).flatMap(([key, item]) => {
15
- const label = key.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/_/g, ' ');
15
+ const label = key.replace(/([a-z])([A-Z])/g, (_, first, upper) => `${first} ${upper.toLowerCase()}`).replace(/_/g, ' ');
16
16
  const children = lines(item, depth + 1);
17
17
  return children.length === 1 ? [`${label}: ${children[0]}`] : [`${label}:`, ...children.map(line => ` ${line}`)];
18
18
  }).concat(entries.length > 30 ? [`… ${entries.length - 30} more fields in the structured result`] : []);
@@ -30,5 +30,5 @@ export function formatRunReport(report, scripted = false) {
30
30
  const trace = report.traceTruncated ? `\nTrace retention is partial${report.trace ? ` (${report.trace.droppedEvents} valid events omitted, ${report.trace.rejectedEvents} rejected)` : ''}; this does not mean the output is truncated.` : '';
31
31
  const clipped = body.length > 12_000 ? `${body.slice(0, 12_000)}\n… Full output is in the structured result.` : body;
32
32
  const status = report.status === 'escalated' ? 'stopped — needs attention (escalated)' : report.status;
33
- return cleanText(`${scripted ? 'Scripted demo' : 'Workflow'}: ${status}${report.error ? ` (${report.error.code})` : ''}\n\n${clipped}${recovery}${uncertain}${trace}\n\nWorkflow calls: ${report.calls.tool} direct tool call${report.calls.tool === 1 ? '' : 's'}, ${report.calls.judge} system one decision${report.calls.judge === 1 ? '' : 's'}, ${report.calls.agent} model step${report.calls.agent === 1 ? '' : 's'}.${scripted ? '\nScripted responses; no model calls. Replay: /agentrun run. Real models: /agentrun demo live.' : ''}`);
33
+ return cleanText(`${scripted ? 'Scripted demo' : 'Workflow'}: ${status}${report.error ? ` (${report.error.code})` : ''}\n\n${clipped}${recovery}${uncertain}${trace}\n\nWorkflow calls: ${report.calls.tool} direct tool call${report.calls.tool === 1 ? '' : 's'}, ${report.calls.judge} system one decision${report.calls.judge === 1 ? '' : 's'}, ${report.calls.agent} model step${report.calls.agent === 1 ? '' : 's'}.${scripted ? '\nScripted responses; no model calls. Replay: /agentrun run. Example modes: /agentrun help.' : ''}`);
34
34
  }
@@ -0,0 +1,6 @@
1
+ export declare class PiRunError extends Error {
2
+ readonly reason: "aborted" | "timeout" | "turn_limit" | "submission_limit" | "no_submission" | "model_error";
3
+ readonly turns: number;
4
+ readonly submissions: number;
5
+ constructor(reason: "aborted" | "timeout" | "turn_limit" | "submission_limit" | "no_submission" | "model_error", turns: number, submissions: number);
6
+ }
@@ -0,0 +1,13 @@
1
+ // Kept apart from runner.ts so the workflow service loads without a pi package installed.
2
+ export class PiRunError extends Error {
3
+ reason;
4
+ turns;
5
+ submissions;
6
+ constructor(reason, turns, submissions) {
7
+ super(`Pi node did not deliver an accepted submission: ${reason} (${turns} turns, ${submissions} submissions)`);
8
+ this.reason = reason;
9
+ this.turns = turns;
10
+ this.submissions = submissions;
11
+ this.name = "PiRunError";
12
+ }
13
+ }
package/dist/runner.d.ts CHANGED
@@ -3,10 +3,5 @@ import type { PiRunnerOptions } from "./types.js";
3
3
  export type { PiRunnerOptions, PiSession, PiSessionFactory } from "./types.js";
4
4
  export type PiRunner = NonNullable<WorkflowDeps["runNode"]>;
5
5
  export type PiNodeRequest = Parameters<PiRunner>[0];
6
- export declare class PiRunError extends Error {
7
- readonly reason: "aborted" | "timeout" | "turn_limit" | "submission_limit" | "no_submission" | "model_error";
8
- readonly turns: number;
9
- readonly submissions: number;
10
- constructor(reason: "aborted" | "timeout" | "turn_limit" | "submission_limit" | "no_submission" | "model_error", turns: number, submissions: number);
11
- }
6
+ export { PiRunError } from "./run-error.js";
12
7
  export declare function createPiRunner(options: PiRunnerOptions): PiRunner;
package/dist/runner.js CHANGED
@@ -1,17 +1,8 @@
1
1
  import { Compile } from "typebox/compile";
2
2
  import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, ModelRuntime, } from "@earendil-works/pi-coding-agent";
3
- export class PiRunError extends Error {
4
- reason;
5
- turns;
6
- submissions;
7
- constructor(reason, turns, submissions) {
8
- super(`Pi node did not deliver an accepted submission: ${reason} (${turns} turns, ${submissions} submissions)`);
9
- this.reason = reason;
10
- this.turns = turns;
11
- this.submissions = submissions;
12
- this.name = "PiRunError";
13
- }
14
- }
3
+ import { PiRunError } from "./run-error.js";
4
+ import { nodeThinking } from "./thinking.js";
5
+ export { PiRunError } from "./run-error.js";
15
6
  function positive(value, name) {
16
7
  if (!Number.isSafeInteger(value) || value < 1)
17
8
  throw new Error(`${name} must be a positive integer`);
@@ -70,6 +61,9 @@ export function createPiRunner(options) {
70
61
  for (const name of requested)
71
62
  if (!names.has(name))
72
63
  throw new Error(`Pi tool was not explicitly registered: ${name}`);
64
+ // Every Pi node starts a fresh session, so there is no caller transcript to fork.
65
+ if (request.context === "fork")
66
+ throw new Error(`Pi node "${request.label}" asks for context: "fork", but the Pi runner starts every node in a fresh session and has no caller transcript to fork. Declare context: "fresh", or run it on a host that supports fork.`);
73
67
  const validator = Compile(request.schema);
74
68
  const parameters = submitParameters(request.schema);
75
69
  const envelopeValidator = Compile(parameters);
@@ -166,7 +160,7 @@ export function createPiRunner(options) {
166
160
  const sessionManager = SessionManager.inMemory(cwd);
167
161
  const prepared = {
168
162
  cwd, model: options.model, modelRuntime: options.modelRuntime,
169
- thinkingLevel: request.thinking ?? options.thinkingLevel ?? "low", maxTurns: maxTurns ?? undefined, resourceLoader, settingsManager,
163
+ thinkingLevel: nodeThinking(request.thinking, options.thinkingLevel), maxTurns: maxTurns ?? undefined, resourceLoader, settingsManager,
170
164
  sessionManager,
171
165
  tools: [...requested, "submit"],
172
166
  customTools: [...(options.tools ?? []).filter(tool => requested.includes(tool.name)).map(tool => ({
@@ -0,0 +1,9 @@
1
+ export { checkKeys, checkSetupPrompt, JEV_KEYS } from "./spec.js";
2
+ export type { CheckOptions, Choice, DemoSpec, EyesNeed, Key, KeyReport, ModelNeed, Package, Platform, Program, ReportItem, RunMode, SetupPromptReport } from "./spec.js";
3
+ export { AGENT_NAME, CHATGPT_PROVIDER, consoleInteraction, signIn, SignInError } from "./signin.js";
4
+ export type { ConsoleIo, SignedIn, SignInOptions } from "./signin.js";
5
+ export { CHATGPT_UNSUPPORTED, resolveModel, ResolveError } from "./resolve.js";
6
+ export type { EyesModel, ResolvedModel, ResolveOptions } from "./resolve.js";
7
+ export { USAGE_LIMIT_CODE, UsageLimitError, usageLimitError } from "./limit.js";
8
+ export { agentrunHome, AuthStoreError, FileCredentialStore, fileCredentialStore } from "./store.js";
9
+ export type { FileCredentialStoreOptions } from "./store.js";
@@ -0,0 +1,7 @@
1
+ // Sign in, pick a model, check keys: the shared piece every demo runs first. The only entry of this package that
2
+ // reaches pi-ai, an optional peer; every other entry stays free of it.
3
+ export { checkKeys, checkSetupPrompt, JEV_KEYS } from "./spec.js";
4
+ export { AGENT_NAME, CHATGPT_PROVIDER, consoleInteraction, signIn, SignInError } from "./signin.js";
5
+ export { CHATGPT_UNSUPPORTED, resolveModel, ResolveError } from "./resolve.js";
6
+ export { USAGE_LIMIT_CODE, UsageLimitError, usageLimitError } from "./limit.js";
7
+ export { agentrunHome, AuthStoreError, FileCredentialStore, fileCredentialStore } from "./store.js";
@@ -0,0 +1,10 @@
1
+ export declare const USAGE_LIMIT_CODE = "subscription_sharing_usage_limit_exceeded";
2
+ export declare class UsageLimitError extends Error {
3
+ readonly code = "USAGE_LIMIT";
4
+ constructor(options?: {
5
+ cause?: unknown;
6
+ });
7
+ }
8
+ /** The typed limit error when `value` (an error, an assistant message from a failed request, or its text) is the plan's
9
+ * usage limit; undefined otherwise. */
10
+ export declare function usageLimitError(value: unknown): UsageLimitError | undefined;
@@ -0,0 +1,23 @@
1
+ // The ChatGPT plan's usage limit (five hours on Plus, shared with every other app the user signed in to). OpenAI answers
2
+ // a request past it with 429 `subscription_sharing_usage_limit_exceeded`, at the start or mid-stream. It resets after
3
+ // hours, not seconds, so it is never retried: pi-ai sends a request once by default and classes the code as not
4
+ // retryable, and this maps it to one typed error.
5
+ export const USAGE_LIMIT_CODE = "subscription_sharing_usage_limit_exceeded";
6
+ export class UsageLimitError extends Error {
7
+ code = "USAGE_LIMIT";
8
+ constructor(options) {
9
+ super("Your ChatGPT plan's usage limit is reached; wait, or set a provider key.", options);
10
+ this.name = "UsageLimitError";
11
+ }
12
+ }
13
+ /** The typed limit error when `value` (an error, an assistant message from a failed request, or its text) is the plan's
14
+ * usage limit; undefined otherwise. */
15
+ export function usageLimitError(value) {
16
+ if (value instanceof UsageLimitError)
17
+ return value;
18
+ const text = typeof value === "string" ? value
19
+ : value instanceof Error ? value.message
20
+ : value && typeof value === "object" && "errorMessage" in value ? String(value.errorMessage ?? "")
21
+ : "";
22
+ return text.includes(USAGE_LIMIT_CODE) ? new UsageLimitError({ cause: value }) : undefined;
23
+ }
@@ -0,0 +1,47 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { type SignInOptions } from "./signin.js";
3
+ import type { DemoSpec } from "./spec.js";
4
+ /** What Sign in with ChatGPT covers: streaming Responses calls with `store: false`, without these. */
5
+ export declare const CHATGPT_UNSUPPORTED: readonly string[];
6
+ export type ResolveOptions = Omit<SignInOptions, "home"> & {
7
+ provider?: string;
8
+ model?: string;
9
+ env?: NodeJS.ProcessEnv;
10
+ home?: string;
11
+ /** An explicit eyes model (`--eyes-model`); `model` defaults to the spec's eyes default. */
12
+ eyes?: {
13
+ provider: string;
14
+ model?: string;
15
+ };
16
+ signal?: AbortSignal;
17
+ };
18
+ type Resolved = {
19
+ model: Model<Api>;
20
+ /** The key valid now. Not enumerable, and left out of `toJSON`. */
21
+ readonly apiKey: string;
22
+ /** A key valid at the time of the call: a sign-in's token is refreshed under the store's lock when it is near expiry. */
23
+ getApiKey(): Promise<string>;
24
+ /** Only streaming Responses calls with `store: false`, without `unsupported`. */
25
+ responsesOnly: boolean;
26
+ unsupported: readonly string[];
27
+ toJSON(): Record<string, unknown>;
28
+ };
29
+ export type ResolvedModel = Resolved & {
30
+ source: "flag" | "chatgpt";
31
+ limit?: string;
32
+ eyes?: EyesModel;
33
+ };
34
+ export type EyesModel = Resolved & {
35
+ source: "flag" | "lead";
36
+ };
37
+ export declare class ResolveError extends Error {
38
+ readonly code: "FLAGS" | "UNKNOWN_PROVIDER" | "UNKNOWN_MODEL" | "MISSING_KEY" | "NEEDS" | "EYES" | "NO_MODEL" | "MODEL_LIST" | "SIGNED_OUT";
39
+ constructor(code: "FLAGS" | "UNKNOWN_PROVIDER" | "UNKNOWN_MODEL" | "MISSING_KEY" | "NEEDS" | "EYES" | "NO_MODEL" | "MODEL_LIST" | "SIGNED_OUT", message: string);
40
+ }
41
+ /**
42
+ * The model this run uses and the key to send, in the order above. The eyes model, when the spec has one, resolves the
43
+ * same way: an explicit eyes model with its provider's key, else the lead's model when it meets the eyes' needs, else
44
+ * a `ResolveError` ("EYES") naming the gap. The ChatGPT plan takes no video input.
45
+ */
46
+ export declare function resolveModel(spec: DemoSpec, options?: ResolveOptions): Promise<ResolvedModel>;
47
+ export {};
@@ -0,0 +1,163 @@
1
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
2
+ import { USAGE_LIMIT_CODE, UsageLimitError } from "./limit.js";
3
+ import { CHATGPT_PROVIDER, chatgptModels, consoleInteraction, signIn } from "./signin.js";
4
+ import { agentrunHome, fileCredentialStore } from "./store.js";
5
+ /** What Sign in with ChatGPT covers: streaming Responses calls with `store: false`, without these. */
6
+ export const CHATGPT_UNSUPPORTED = Object.freeze(["temperature", "max_output_tokens", "previous_response_id", "hosted tools", "audio input", "video input"]);
7
+ export class ResolveError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "ResolveError";
13
+ }
14
+ }
15
+ const meets = (model, need) => need === "images" ? model.input.includes("image") : need !== "video";
16
+ function resolved(model, apiKey, getApiKey, source, extra = {}) {
17
+ const responsesOnly = source === "chatgpt" || extra.responsesOnly === true;
18
+ const result = { model, source, responsesOnly, unsupported: responsesOnly ? CHATGPT_UNSUPPORTED : [], ...extra };
19
+ Object.defineProperty(result, "apiKey", { value: apiKey, enumerable: false });
20
+ Object.defineProperty(result, "getApiKey", { value: getApiKey, enumerable: false });
21
+ Object.defineProperty(result, "toJSON", {
22
+ enumerable: false,
23
+ value: () => {
24
+ const { model: _model, ...rest } = result;
25
+ return { provider: model.provider, model: model.id, ...rest, ...(rest.eyes ? { eyes: rest.eyes.toJSON() } : {}) };
26
+ },
27
+ });
28
+ return result;
29
+ }
30
+ /** The env key of `provider` set in `env`, and the names it was looked up under. Anthropic takes an API key only. */
31
+ async function providerKey(provider, env, signal) {
32
+ if (provider.id === "anthropic")
33
+ return { key: env.ANTHROPIC_API_KEY || undefined, names: ["ANTHROPIC_API_KEY"] };
34
+ const names = [];
35
+ const result = await provider.auth.apiKey?.resolve({
36
+ ctx: { env: async (name) => { names.push(name); return env[name] || undefined; }, fileExists: async () => false },
37
+ signal,
38
+ });
39
+ return { key: result?.auth.apiKey, names };
40
+ }
41
+ /** The account's models (GET /v1/models), those it lists as visible. */
42
+ async function accountModels(baseUrl, apiKey, signal) {
43
+ const response = await fetch(`${baseUrl}/models`, { headers: { authorization: `Bearer ${apiKey}` }, signal });
44
+ if (!response.ok) {
45
+ const body = await response.text().catch(() => "");
46
+ if (body.includes(USAGE_LIMIT_CODE))
47
+ throw new UsageLimitError();
48
+ throw new ResolveError("MODEL_LIST", `GET ${baseUrl}/models answered ${response.status}`);
49
+ }
50
+ const data = (await response.json()).data ?? [];
51
+ return data
52
+ .filter((entry) => typeof entry?.id === "string" && (entry.visibility === undefined || entry.visibility === "list"))
53
+ .map((entry) => ({ id: entry.id, created: typeof entry.created === "number" ? entry.created : 0 }));
54
+ }
55
+ /** The preferred id when it is listed and capable, else the newest listed model pi-ai knows that meets `needs`. */
56
+ function choose(provider, listed, needs, preferred) {
57
+ const known = new Map(provider.getModels().map((model) => [model.id, model]));
58
+ const capable = listed
59
+ .flatMap((entry) => { const model = known.get(entry.id); return model && needs.every((need) => meets(model, need)) ? [{ model, created: entry.created }] : []; })
60
+ .sort((a, b) => b.created - a.created || a.model.id.localeCompare(b.model.id));
61
+ const pick = capable.find((entry) => entry.model.id === preferred) ?? capable[0];
62
+ if (!pick)
63
+ throw new ResolveError("NO_MODEL", `The account lists ${listed.length} models, and none is a ${provider.name} model pi-ai knows that takes ${needs.join(", ")}.`);
64
+ return pick.model;
65
+ }
66
+ async function flagModel(providerId, modelId, needs, preferred, env, signal, flag) {
67
+ const provider = builtinProviders().find((entry) => entry.id === providerId);
68
+ if (!provider)
69
+ throw new ResolveError("UNKNOWN_PROVIDER", `${flag} ${providerId}: pi-ai has no provider with that id`);
70
+ const { key, names } = await providerKey(provider, env, signal);
71
+ if (!key)
72
+ throw new ResolveError("MISSING_KEY", `${flag} ${providerId} needs ${names.join(" or ") || "its key"} set`);
73
+ // Video cannot be read from pi-ai's catalog: an explicit model is trusted to read it.
74
+ const checked = needs.filter((need) => need !== "video");
75
+ let model;
76
+ if (modelId) {
77
+ model = provider.getModels().find((entry) => entry.id === modelId);
78
+ if (!model)
79
+ throw new ResolveError("UNKNOWN_MODEL", `${provider.name} has no model ${modelId} in pi-ai's catalog`);
80
+ const gap = checked.filter((need) => !meets(model, need));
81
+ if (gap.length)
82
+ throw new ResolveError("NEEDS", `${provider.name} ${modelId} does not take ${gap.join(" or ")} input, which this run needs`);
83
+ }
84
+ else if (providerId === CHATGPT_PROVIDER && provider.baseUrl) {
85
+ model = choose(provider, await accountModels(provider.baseUrl, key, signal), checked, preferred);
86
+ }
87
+ else {
88
+ const fallback = preferred ? provider.getModels().find((entry) => entry.id === preferred) : undefined;
89
+ if (!fallback || !checked.every((need) => meets(fallback, need)))
90
+ throw new ResolveError("FLAGS", `${flag} ${providerId} needs a model id too`);
91
+ model = fallback;
92
+ }
93
+ return { model, key };
94
+ }
95
+ /**
96
+ * The model this run uses and the key to send, in the order above. The eyes model, when the spec has one, resolves the
97
+ * same way: an explicit eyes model with its provider's key, else the lead's model when it meets the eyes' needs, else
98
+ * a `ResolveError` ("EYES") naming the gap. The ChatGPT plan takes no video input.
99
+ */
100
+ export async function resolveModel(spec, options = {}) {
101
+ const env = options.env ?? process.env;
102
+ const signal = options.signal ?? new AbortController().signal;
103
+ const needs = spec.models.lead.needs;
104
+ let lead;
105
+ if (options.provider || options.model) {
106
+ if (!options.provider)
107
+ throw new ResolveError("FLAGS", "--model needs --provider");
108
+ const { model, key } = await flagModel(options.provider, options.model, needs, spec.models.lead.default, env, signal, "--provider");
109
+ lead = resolved(model, key, async () => key, "flag");
110
+ }
111
+ else {
112
+ const home = agentrunHome(options.home, env);
113
+ const store = fileCredentialStore(home);
114
+ if ((await store.read(CHATGPT_PROVIDER, { signal }))?.type !== "oauth") {
115
+ // The run's signal reaches the sign-in through its interaction, so a cancel ends the login and frees its port.
116
+ const interaction = options.interaction ?? consoleInteraction();
117
+ // Its calls are forwarded, not spread: an interaction's notify and prompt may be methods of its class.
118
+ const cancelled = options.signal ? {
119
+ signal: interaction.signal ? AbortSignal.any([interaction.signal, options.signal]) : options.signal,
120
+ notify: (event) => interaction.notify(event),
121
+ prompt: (prompt) => interaction.prompt(prompt),
122
+ } : interaction;
123
+ try {
124
+ await signIn({ ...options, home, env, interaction: cancelled });
125
+ }
126
+ catch (error) {
127
+ if (options.signal?.aborted)
128
+ throw options.signal.reason;
129
+ throw error;
130
+ }
131
+ }
132
+ const models = chatgptModels(store);
133
+ const getApiKey = async () => {
134
+ const key = (await models.getAuth(CHATGPT_PROVIDER, { signal }))?.auth.apiKey;
135
+ if (!key)
136
+ throw new ResolveError("SIGNED_OUT", "The ChatGPT sign-in is gone from auth.json; sign in again");
137
+ return key;
138
+ };
139
+ const key = await getApiKey();
140
+ const provider = models.getProvider(CHATGPT_PROVIDER);
141
+ const model = choose(provider, await accountModels(provider.baseUrl, key, signal), needs, spec.models.lead.default);
142
+ lead = resolved(model, key, getApiKey, "chatgpt", {
143
+ limit: `Model calls count toward your ChatGPT plan's usage limit (five hours on Plus, shared with your other apps); this run expects about ${spec.expectedCalls}.`,
144
+ });
145
+ }
146
+ const eyes = spec.models.eyes;
147
+ if (!eyes)
148
+ return lead;
149
+ if (options.eyes) {
150
+ const { model, key } = await flagModel(options.eyes.provider, options.eyes.model ?? eyes.default, eyes.needs, eyes.default, env, signal, "--eyes-model");
151
+ lead.eyes = resolved(model, key, async () => key, "flag");
152
+ return lead;
153
+ }
154
+ const gap = eyes.needs.filter((need) => !meets(lead.model, need));
155
+ if (gap.length) {
156
+ const reasons = gap.map((need) => need === "images" ? `the lead model ${lead.model.id} takes no image input`
157
+ : lead.source === "chatgpt" ? "the ChatGPT plan takes no video input"
158
+ : `pi-ai's catalog lists no video input for ${lead.model.provider} ${lead.model.id}`);
159
+ throw new ResolveError("EYES", `The run's eyes need ${gap.join(" and ")} input: ${reasons.join("; ")}. Name a model that reads it with --eyes-model and set its provider's key${gap.includes("video") ? ", or list images and let frames be extracted locally" : ""}.`);
160
+ }
161
+ lead.eyes = resolved(lead.model, lead.apiKey, lead.getApiKey, "lead", { responsesOnly: lead.responsesOnly });
162
+ return lead;
163
+ }
@@ -0,0 +1,44 @@
1
+ import type { AuthInteraction, MutableModels } from "@earendil-works/pi-ai";
2
+ import { type FileCredentialStore } from "./store.js";
3
+ export declare const AGENT_NAME = "AgentRun";
4
+ /** pi-ai's provider id for OpenAI, whose OAuth method is Sign in with ChatGPT. */
5
+ export declare const CHATGPT_PROVIDER = "openai";
6
+ export type SignInOptions = {
7
+ home?: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ /** The app name OpenAI shows on the consent screen. Default "AgentRun". */
10
+ agentName?: string;
11
+ /** How the sign-in talks to the user. Default: the terminal (`consoleInteraction`). */
12
+ interaction?: AuthInteraction;
13
+ };
14
+ export type SignedIn = {
15
+ home: string;
16
+ provider: typeof CHATGPT_PROVIDER;
17
+ hostId: string;
18
+ expires: number;
19
+ };
20
+ export declare class SignInError extends Error {
21
+ readonly code: "PORT_IN_USE" | "APP_NAME_IGNORED";
22
+ constructor(code: "PORT_IN_USE" | "APP_NAME_IGNORED", message: string, options?: {
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ /** A pi-ai collection with only OpenAI, whose credentials live in `store`. */
27
+ export declare function chatgptModels(store: FileCredentialStore): MutableModels;
28
+ /**
29
+ * Sign in once per install; the token refreshes on its own afterwards. The sign-in page must name this app: pi-ai
30
+ * before 1.1 ignores `agentName` and names pi, so a page that names anything else is never shown, and the sign-in fails
31
+ * with `SignInError` ("APP_NAME_IGNORED") before the browser opens.
32
+ */
33
+ export declare function signIn(options?: SignInOptions): Promise<SignedIn>;
34
+ export type ConsoleIo = {
35
+ input?: NodeJS.ReadableStream & {
36
+ isTTY?: boolean;
37
+ };
38
+ output?: NodeJS.WritableStream;
39
+ /** Opens the sign-in page; default the platform's opener, skipped when AGENTRUN_NO_BROWSER is set. */
40
+ open?: (url: string) => void;
41
+ };
42
+ /** The terminal: prints the sign-in page's address and opens it, then waits for the browser's callback or, on a
43
+ * terminal, a pasted redirect address. The page address carries no token. */
44
+ export declare function consoleInteraction(io?: ConsoleIo): AuthInteraction;