operon-agents 0.1.0-alpha.1 → 0.1.0-alpha.10

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
@@ -16,3 +16,45 @@ import { TransportMCPServer } from "operon-agents/mcp";
16
16
  import { bashTool, readTool } from "operon-agents/tools";
17
17
  import { OTelTracingProcessor } from "operon-agents/tracing";
18
18
  ```
19
+
20
+ ## Tracing
21
+
22
+ Every session drives the `TracingProcessor` the `harness` hook returns as `tracing`: one
23
+ trace per run (a prompt and everything it triggers), spans for agents, turns, model generations,
24
+ tool calls and sub-agents, the session id on every span as `gen_ai.conversation.id`. By default
25
+ spans are metadata only. Pass `content: "delta"` to also record the system prompt, the messages
26
+ the model was asked to answer, its output, tool arguments and results — enough to replay a
27
+ conversation in a trace viewer. Local Jaeger:
28
+
29
+ ```sh
30
+ docker run --rm -p 16686:16686 -p 4318:4318 jaegertracing/jaeger:2.9.0
31
+ ```
32
+
33
+ ```ts
34
+ import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
35
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
36
+ import { resourceFromAttributes } from "@opentelemetry/resources";
37
+ import { OTelTracingProcessor } from "operon-agents/tracing";
38
+
39
+ const provider = new NodeTracerProvider({
40
+ resource: resourceFromAttributes({ "service.name": "my-agent" }),
41
+ spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" }))],
42
+ });
43
+ const tracing = new OTelTracingProcessor({ tracer: provider.getTracer("my-agent"), tracerProvider: provider, content: "delta", shutdownProvider: true });
44
+
45
+ const harness = await createLocalHarness({
46
+ model,
47
+ harness: () => ({ tracing }),
48
+ });
49
+ ```
50
+
51
+ `content: "full"` repeats the entire context on every generation (trace size grows with steps ×
52
+ context); `contentMaxChars` caps each attribute; `redact: true` masks tokens, emails and keys
53
+ before export when the collector is not on the same machine.
54
+
55
+ A run that pauses for an answer (`turn.paused`) ends its trace at the pause — the turn with
56
+ reason `paused`, the suspended tool calls marked, not failed — and the run that continues it is
57
+ the next trace of the same session. Spans are exported when they end, so a wait that may last
58
+ days is never an open span. `examples/linear-github/server/langfuse.ts` shows the same
59
+ processor exporting to Langfuse.
60
+
@@ -9,6 +9,7 @@
9
9
  import { spawn } from "node:child_process";
10
10
  import { classify, isFailure, nodeStreamTransport, request as makeRequest, RpcError, success, } from "./codec.js";
11
11
  import { Method, PROTOCOL_VERSION, } from "./protocol.js";
12
+ import { ClientCancelRequestParamsSchema, ClientRequestApprovalParamsSchema, ClientRequestQuestionParamsSchema, parseParams, } from "./params.js";
12
13
  export class AppServerClient {
13
14
  transport;
14
15
  child;
@@ -66,9 +67,11 @@ export class AppServerClient {
66
67
  return;
67
68
  }
68
69
  if (method === Method.ClientCancelRequest) {
69
- const { id } = (params ?? {});
70
- this.inboundAbort.get(id)?.abort();
71
- this.inboundAbort.delete(id);
70
+ const parsed = ClientCancelRequestParamsSchema.safeParse(params);
71
+ if (!parsed.success)
72
+ return;
73
+ this.inboundAbort.get(parsed.data.id)?.abort();
74
+ this.inboundAbort.delete(parsed.data.id);
72
75
  }
73
76
  }
74
77
  async onReverseRequest(id, method, params) {
@@ -88,13 +91,13 @@ export class AppServerClient {
88
91
  }
89
92
  async dispatchReverse(method, params, signal) {
90
93
  if (method === Method.ClientRequestApproval) {
91
- const p = params;
94
+ const p = parseParams(ClientRequestApprovalParamsSchema, params);
92
95
  if (!this.approvalHandler)
93
96
  return { decision: "rejected", feedback: "no approval handler" };
94
97
  return this.approvalHandler(p.request, p.sessionId);
95
98
  }
96
99
  if (method === Method.ClientRequestQuestion) {
97
- const p = params;
100
+ const p = parseParams(ClientRequestQuestionParamsSchema, params);
98
101
  if (!this.questionHandler)
99
102
  return null;
100
103
  return this.questionHandler(p.request, p.sessionId, { signal });
@@ -63,7 +63,7 @@ async function main() {
63
63
  const harness = createHarness({
64
64
  model: args.model ?? "anthropic/claude-opus-4-8",
65
65
  resolveModel,
66
- ...(args.home !== undefined ? { repository: new DiskSessionRepository(args.home) } : {}),
66
+ ...(args.home !== undefined ? { harness: () => ({ sessionRepository: new DiskSessionRepository(args.home) }) } : {}),
67
67
  ...(args.workdir !== undefined ? { workDir: args.workdir } : {}),
68
68
  permission: { mode: args.permission ?? "yolo" },
69
69
  });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Runtime shape of every host → server request and server → host reverse request in
3
+ * `protocol.ts`. That module stays pure types so other languages can mirror it; this one is the
4
+ * TypeScript side's proof that a message read off the wire actually has that shape before a
5
+ * handler touches it. A miss is an `InvalidParams` error that names the field.
6
+ */
7
+ import { z } from "zod";
8
+ import type { Message, QuestionItem } from "operon-agents-core";
9
+ import type { ClientCancelRequestParams, ClientRequestApprovalParams, ClientRequestQuestionParams, InitializeParams, SessionFollowUpParams, SessionForkParams, SessionInvokeParams, SessionNewParams, SessionPromptParams, SessionRef, SessionRespondParams, SessionResumeParams, SessionSnapshotParams, SessionSteerParams } from "./protocol.ts";
10
+ export declare const SessionRefSchema: z.ZodObject<{
11
+ sessionId: z.ZodString;
12
+ }, z.core.$strip>;
13
+ export declare const InitializeParamsSchema: z.ZodObject<{
14
+ protocolVersion: z.ZodNumber;
15
+ clientInfo: z.ZodOptional<z.ZodObject<{
16
+ name: z.ZodString;
17
+ version: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>>;
19
+ clientCapabilities: z.ZodOptional<z.ZodObject<{
20
+ approval: z.ZodOptional<z.ZodBoolean>;
21
+ question: z.ZodOptional<z.ZodBoolean>;
22
+ }, z.core.$strip>>;
23
+ }, z.core.$strip>;
24
+ export declare const SessionNewParamsSchema: z.ZodObject<{
25
+ workDir: z.ZodOptional<z.ZodString>;
26
+ title: z.ZodOptional<z.ZodString>;
27
+ model: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$strip>;
29
+ export declare const SessionResumeParamsSchema: z.ZodObject<{
30
+ sessionId: z.ZodString;
31
+ }, z.core.$strip>;
32
+ export declare const SessionForkParamsSchema: z.ZodObject<{
33
+ title: z.ZodOptional<z.ZodString>;
34
+ sessionId: z.ZodString;
35
+ }, z.core.$strip>;
36
+ export declare const SessionCloseParamsSchema: z.ZodObject<{
37
+ sessionId: z.ZodString;
38
+ }, z.core.$strip>;
39
+ export declare const SessionCancelParamsSchema: z.ZodObject<{
40
+ sessionId: z.ZodString;
41
+ }, z.core.$strip>;
42
+ export declare const SessionPromptParamsSchema: z.ZodObject<{
43
+ input: z.ZodUnion<readonly [z.ZodString, z.ZodCustom<Message[], Message[]>]>;
44
+ sessionId: z.ZodString;
45
+ }, z.core.$strip>;
46
+ export declare const SessionSnapshotParamsSchema: z.ZodObject<{
47
+ addresses: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
48
+ maxMessages: z.ZodOptional<z.ZodNumber>;
49
+ sessionId: z.ZodString;
50
+ }, z.core.$strip>;
51
+ export declare const SessionRespondParamsSchema: z.ZodObject<{
52
+ answers: z.ZodRecord<z.ZodString, z.ZodObject<{
53
+ decision: z.ZodEnum<{
54
+ approved: "approved";
55
+ rejected: "rejected";
56
+ cancelled: "cancelled";
57
+ }>;
58
+ scope: z.ZodOptional<z.ZodLiteral<"session">>;
59
+ feedback: z.ZodOptional<z.ZodString>;
60
+ }, z.core.$strip>>;
61
+ sessionId: z.ZodString;
62
+ }, z.core.$strip>;
63
+ export declare const SessionSteerParamsSchema: z.ZodObject<{
64
+ text: z.ZodString;
65
+ sessionId: z.ZodString;
66
+ }, z.core.$strip>;
67
+ export declare const SessionFollowUpParamsSchema: z.ZodObject<{
68
+ text: z.ZodString;
69
+ sessionId: z.ZodString;
70
+ }, z.core.$strip>;
71
+ export declare const SessionInvokeParamsSchema: z.ZodObject<{
72
+ method: z.ZodString;
73
+ args: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodUnknown>>>;
74
+ sessionId: z.ZodString;
75
+ }, z.core.$strip>;
76
+ export declare const ClientRequestApprovalParamsSchema: z.ZodObject<{
77
+ request: z.ZodObject<{
78
+ toolCallId: z.ZodString;
79
+ toolName: z.ZodString;
80
+ approvalRule: z.ZodString;
81
+ }, z.core.$loose>;
82
+ sessionId: z.ZodString;
83
+ }, z.core.$strip>;
84
+ export declare const ClientRequestQuestionParamsSchema: z.ZodObject<{
85
+ request: z.ZodObject<{
86
+ turnId: z.ZodString;
87
+ toolCallId: z.ZodString;
88
+ questions: z.ZodCustom<readonly QuestionItem[], readonly QuestionItem[]>;
89
+ }, z.core.$loose>;
90
+ sessionId: z.ZodString;
91
+ }, z.core.$strip>;
92
+ export declare const ClientCancelRequestParamsSchema: z.ZodObject<{
93
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
94
+ }, z.core.$strip>;
95
+ /** Parse `params` against a method's schema; a miss is `InvalidParams` naming the field. */
96
+ export declare function parseParams<S extends z.ZodType>(schema: S, params: unknown): z.output<S>;
97
+ export declare const INVOKABLE: Set<string>;
98
+ type Assert<T extends true> = T;
99
+ type Extends<A, B> = A extends B ? true : false;
100
+ export type _Initialize = Assert<Extends<InitializeParams, z.input<typeof InitializeParamsSchema>>>;
101
+ export type _SessionNew = Assert<Extends<SessionNewParams, z.input<typeof SessionNewParamsSchema>>>;
102
+ export type _SessionResume = Assert<Extends<SessionResumeParams, z.input<typeof SessionResumeParamsSchema>>>;
103
+ export type _SessionFork = Assert<Extends<SessionForkParams, z.input<typeof SessionForkParamsSchema>>>;
104
+ export type _SessionRef = Assert<Extends<SessionRef, z.input<typeof SessionRefSchema>>>;
105
+ export type _SessionPrompt = Assert<Extends<SessionPromptParams, z.input<typeof SessionPromptParamsSchema>>>;
106
+ export type _SessionSnapshot = Assert<Extends<SessionSnapshotParams, z.input<typeof SessionSnapshotParamsSchema>>>;
107
+ export type _SessionRespond = Assert<Extends<SessionRespondParams, z.input<typeof SessionRespondParamsSchema>>>;
108
+ export type _SessionSteer = Assert<Extends<SessionSteerParams, z.input<typeof SessionSteerParamsSchema>>>;
109
+ export type _SessionFollowUp = Assert<Extends<SessionFollowUpParams, z.input<typeof SessionFollowUpParamsSchema>>>;
110
+ export type _SessionInvoke = Assert<Extends<SessionInvokeParams, z.input<typeof SessionInvokeParamsSchema>>>;
111
+ export type _ClientRequestApproval = Assert<Extends<z.output<typeof ClientRequestApprovalParamsSchema>, ClientRequestApprovalParams>>;
112
+ export type _ClientRequestQuestion = Assert<Extends<z.output<typeof ClientRequestQuestionParamsSchema>, ClientRequestQuestionParams>>;
113
+ export type _ClientCancelRequest = Assert<Extends<ClientCancelRequestParams, z.input<typeof ClientCancelRequestParamsSchema>>>;
114
+ export {};
115
+ //# sourceMappingURL=params.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Runtime shape of every host → server request and server → host reverse request in
3
+ * `protocol.ts`. That module stays pure types so other languages can mirror it; this one is the
4
+ * TypeScript side's proof that a message read off the wire actually has that shape before a
5
+ * handler touches it. A miss is an `InvalidParams` error that names the field.
6
+ */
7
+ import { z } from "zod";
8
+ import { ErrorCode, INVOKABLE_METHODS } from "./protocol.js";
9
+ import { RpcError } from "./codec.js";
10
+ const sessionRef = { sessionId: z.string().min(1, "sessionId required") };
11
+ export const SessionRefSchema = z.object(sessionRef);
12
+ export const InitializeParamsSchema = z.object({
13
+ protocolVersion: z.number({ error: "protocolVersion (number) required" }),
14
+ clientInfo: z.object({ name: z.string(), version: z.string().optional() }).optional(),
15
+ clientCapabilities: z.object({ approval: z.boolean().optional(), question: z.boolean().optional() }).optional(),
16
+ });
17
+ export const SessionNewParamsSchema = z.object({
18
+ workDir: z.string().optional(),
19
+ title: z.string().optional(),
20
+ model: z.string().optional(),
21
+ });
22
+ export const SessionResumeParamsSchema = SessionRefSchema;
23
+ export const SessionForkParamsSchema = z.object({ ...sessionRef, title: z.string().optional() });
24
+ export const SessionCloseParamsSchema = SessionRefSchema;
25
+ export const SessionCancelParamsSchema = SessionRefSchema;
26
+ const isMessageLike = (value) => typeof value === "object" && value !== null && typeof value.role === "string";
27
+ /** `AgentInput`: a prompt string, or a message array whose contents the core validates itself. */
28
+ const AgentInputSchema = z.union([z.string(), z.custom((value) => Array.isArray(value) && value.every(isMessageLike))], { error: "input must be a string or an array of messages" });
29
+ export const SessionPromptParamsSchema = z.object({ ...sessionRef, input: AgentInputSchema });
30
+ export const SessionSnapshotParamsSchema = z.object({
31
+ ...sessionRef,
32
+ addresses: z.array(z.string()).readonly().optional(),
33
+ maxMessages: z.number().int().nonnegative().optional(),
34
+ });
35
+ const ApprovalResponseSchema = z.object({
36
+ decision: z.enum(["approved", "rejected", "cancelled"]),
37
+ scope: z.literal("session").optional(),
38
+ feedback: z.string().optional(),
39
+ });
40
+ export const SessionRespondParamsSchema = z.object({
41
+ ...sessionRef,
42
+ answers: z.record(z.string(), ApprovalResponseSchema),
43
+ });
44
+ export const SessionSteerParamsSchema = z.object({ ...sessionRef, text: z.string() });
45
+ export const SessionFollowUpParamsSchema = z.object({ ...sessionRef, text: z.string() });
46
+ export const SessionInvokeParamsSchema = z.object({
47
+ ...sessionRef,
48
+ method: z.string(),
49
+ args: z.array(z.unknown()).readonly().optional(),
50
+ });
51
+ // ── Reverse-RPC (server → host), validated by the reference client ───────────
52
+ export const ClientRequestApprovalParamsSchema = z.object({
53
+ ...sessionRef,
54
+ request: z.looseObject({ toolCallId: z.string(), toolName: z.string(), approvalRule: z.string() }),
55
+ });
56
+ export const ClientRequestQuestionParamsSchema = z.object({
57
+ ...sessionRef,
58
+ request: z.looseObject({
59
+ turnId: z.string(),
60
+ toolCallId: z.string(),
61
+ questions: z.custom((value) => Array.isArray(value), "questions must be an array"),
62
+ }),
63
+ });
64
+ export const ClientCancelRequestParamsSchema = z.object({ id: z.union([z.string(), z.number()]) });
65
+ /** Parse `params` against a method's schema; a miss is `InvalidParams` naming the field. */
66
+ export function parseParams(schema, params) {
67
+ const result = schema.safeParse(params ?? {});
68
+ if (result.success)
69
+ return result.data;
70
+ throw new RpcError(ErrorCode.InvalidParams, describeIssues(result.error.issues));
71
+ }
72
+ function describeIssues(issues) {
73
+ return issues
74
+ .map((issue) => {
75
+ const path = issue.path.map(String).join(".");
76
+ return path.length > 0 ? `${path}: ${issue.message}` : issue.message;
77
+ })
78
+ .join("; ");
79
+ }
80
+ export const INVOKABLE = new Set(INVOKABLE_METHODS);
81
+ //# sourceMappingURL=params.js.map
@@ -194,11 +194,17 @@ export interface SessionInvokeResult {
194
194
  readonly value: unknown;
195
195
  }
196
196
  /**
197
- * The whitelist of `HarnessSession` methods reachable through `session/invoke`.
198
- * Run-driving (prompt/steer/follow_up/cancel), lifecycle (close), and handler registration
199
- * are deliberately excluded — they are first-class methods or host-side concerns.
197
+ * The whitelist of operations reachable through `session/invoke`.
198
+ *
199
+ * `"<service>.<op>"` names an operation on one of the session's capability services
200
+ * (`session.goal.create`); a bare name is a method on the session itself. Run-driving
201
+ * (prompt/steer/follow_up/cancel), lifecycle (close) and handler registration are deliberately
202
+ * excluded — they are first-class methods or host-side concerns.
203
+ *
204
+ * This list stays explicit, one entry per reachable operation: it is a security boundary, so a
205
+ * new capability operation should have to be named here to become remotely callable.
200
206
  */
201
- export declare const INVOKABLE_METHODS: readonly ["setModel", "setThinking", "setPermissionMode", "createGoal", "getGoal", "pauseGoal", "resumeGoal", "cancelGoal", "setGoalBudget", "setPlanMode", "getPlan", "clearPlan", "compact", "pendingCompaction", "cancelCompaction", "listSkills", "activateSkill", "listPlugins", "getPluginInfo", "installPlugin", "setPluginEnabled", "setPluginMcpServerEnabled", "removePlugin", "reloadPlugins", "listBackgroundTasks", "readBackgroundTaskOutput", "readBackgroundTaskOutputDelta", "stopBackgroundTask", "detachTool", "runCommand", "getRecords", "listSubagents", "reconcileSubagents", "getContextBreakdown", "listMcpServers", "reconnectMcpServer"];
207
+ export declare const INVOKABLE_METHODS: readonly ["setModel", "setThinking", "setPermissionMode", "goal.create", "goal.snapshot", "goal.pause", "goal.resume", "goal.cancel", "goal.changeBudget", "plan.setEnabled", "plan.data", "compaction.request", "compaction.pending", "compaction.cancel", "skills.listSkills", "skills.activateSkill", "plugins.summaries", "plugins.info", "plugins.installSummary", "plugins.setEnabled", "plugins.setMcpServerEnabled", "plugins.remove", "plugins.reload", "background.list", "background.readOutput", "background.readOutputDelta", "background.stop", "background.detach", "background.listSubagents", "background.reconcileSubagents", "background.listWorkflows", "background.getWorkflow", "runCommand", "getRecords", "getContextBreakdown", "mcp.list", "mcp.listTools", "mcp.reconnect"];
202
208
  export type InvokableMethod = (typeof INVOKABLE_METHODS)[number];
203
209
  /** `session/event` — an AgentEvent verbatim (already carries `sessionId` + `address`). */
204
210
  export type SessionEventParams = AgentEvent;
@@ -42,59 +42,66 @@ export const ErrorCode = {
42
42
  RequestCancelled: -32011,
43
43
  };
44
44
  /**
45
- * The whitelist of `HarnessSession` methods reachable through `session/invoke`.
46
- * Run-driving (prompt/steer/follow_up/cancel), lifecycle (close), and handler registration
47
- * are deliberately excluded — they are first-class methods or host-side concerns.
45
+ * The whitelist of operations reachable through `session/invoke`.
46
+ *
47
+ * `"<service>.<op>"` names an operation on one of the session's capability services
48
+ * (`session.goal.create`); a bare name is a method on the session itself. Run-driving
49
+ * (prompt/steer/follow_up/cancel), lifecycle (close) and handler registration are deliberately
50
+ * excluded — they are first-class methods or host-side concerns.
51
+ *
52
+ * This list stays explicit, one entry per reachable operation: it is a security boundary, so a
53
+ * new capability operation should have to be named here to become remotely callable.
48
54
  */
49
55
  export const INVOKABLE_METHODS = [
50
- // runtime settings
56
+ // runtime settings — on the session itself
51
57
  "setModel",
52
58
  "setThinking",
53
59
  "setPermissionMode",
54
60
  // goal
55
- "createGoal",
56
- "getGoal",
57
- "pauseGoal",
58
- "resumeGoal",
59
- "cancelGoal",
60
- "setGoalBudget",
61
+ "goal.create",
62
+ "goal.snapshot",
63
+ "goal.pause",
64
+ "goal.resume",
65
+ "goal.cancel",
66
+ "goal.changeBudget",
61
67
  // plan
62
- "setPlanMode",
63
- "getPlan",
64
- "clearPlan",
68
+ "plan.setEnabled",
69
+ "plan.data",
65
70
  // compaction
66
- "compact",
67
- "pendingCompaction",
68
- "cancelCompaction",
71
+ "compaction.request",
72
+ "compaction.pending",
73
+ "compaction.cancel",
69
74
  // skills
70
- "listSkills",
71
- "activateSkill",
75
+ "skills.listSkills",
76
+ "skills.activateSkill",
72
77
  // plugins
73
- "listPlugins",
74
- "getPluginInfo",
75
- "installPlugin",
76
- "setPluginEnabled",
77
- "setPluginMcpServerEnabled",
78
- "removePlugin",
79
- "reloadPlugins",
80
- // background tasks
81
- "listBackgroundTasks",
82
- "readBackgroundTaskOutput",
83
- "readBackgroundTaskOutputDelta",
84
- "stopBackgroundTask",
85
- "detachTool",
78
+ "plugins.summaries",
79
+ "plugins.info",
80
+ "plugins.installSummary",
81
+ "plugins.setEnabled",
82
+ "plugins.setMcpServerEnabled",
83
+ "plugins.remove",
84
+ "plugins.reload",
85
+ // background tasks, subagents and workflow runs — one ledger, three views
86
+ "background.list",
87
+ "background.readOutput",
88
+ "background.readOutputDelta",
89
+ "background.stop",
90
+ "background.detach",
91
+ "background.listSubagents",
92
+ "background.reconcileSubagents",
93
+ "background.listWorkflows",
94
+ "background.getWorkflow",
86
95
  // slash commands — the serializable control surface for extensions (cron et al.):
87
96
  // per-feature cron RPCs are gone with the capability, /cron rides this instead.
88
97
  "runCommand",
89
98
  // conversation log
90
99
  "getRecords",
91
- // subagents
92
- "listSubagents",
93
- "reconcileSubagents",
94
100
  // context introspection
95
101
  "getContextBreakdown",
96
102
  // mcp
97
- "listMcpServers",
98
- "reconnectMcpServer",
103
+ "mcp.list",
104
+ "mcp.listTools",
105
+ "mcp.reconnect",
99
106
  ];
100
107
  //# sourceMappingURL=protocol.js.map
@@ -45,7 +45,6 @@ export declare class AppServer {
45
45
  private callClient;
46
46
  private resolveReverse;
47
47
  private rejectReverseForSession;
48
- private requireId;
49
48
  private requireSession;
50
49
  }
51
50
  //# sourceMappingURL=server.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { errorToBody, failure, notification, request as makeRequest, RpcError, success, toWireSafe, } from "./codec.js";
2
2
  import { ErrorCode, INVOKABLE_METHODS, Method, PROTOCOL_VERSION, } from "./protocol.js";
3
- const INVOKABLE = new Set(INVOKABLE_METHODS);
3
+ import { INVOKABLE, InitializeParamsSchema, parseParams, SessionCancelParamsSchema, SessionCloseParamsSchema, SessionFollowUpParamsSchema, SessionForkParamsSchema, SessionInvokeParamsSchema, SessionNewParamsSchema, SessionPromptParamsSchema, SessionRespondParamsSchema, SessionResumeParamsSchema, SessionSnapshotParamsSchema, SessionSteerParamsSchema, } from "./params.js";
4
4
  export class AppServer {
5
5
  harness;
6
6
  transport;
@@ -73,43 +73,41 @@ export class AppServer {
73
73
  }
74
74
  async route(method, params) {
75
75
  if (method === Method.Initialize)
76
- return this.onInitialize(params);
76
+ return this.onInitialize(parseParams(InitializeParamsSchema, params));
77
77
  if (!this.initialized)
78
78
  throw new RpcError(ErrorCode.NotInitialized, "call initialize first");
79
+ // Method lookup before params: an unknown method is `MethodNotFound` whatever it carries.
79
80
  switch (method) {
80
81
  case Method.SessionNew:
81
- return this.onSessionNew((params ?? {}));
82
+ return this.onSessionNew(parseParams(SessionNewParamsSchema, params));
82
83
  case Method.SessionResume:
83
- return this.onSessionResume(params);
84
+ return this.onSessionResume(parseParams(SessionResumeParamsSchema, params));
84
85
  case Method.SessionFork:
85
- return this.onSessionFork(params);
86
+ return this.onSessionFork(parseParams(SessionForkParamsSchema, params));
86
87
  case Method.SessionList:
87
88
  return this.harness.listSessions();
88
89
  case Method.SessionClose:
89
- return this.onSessionClose(params);
90
+ return this.onSessionClose(parseParams(SessionCloseParamsSchema, params));
90
91
  case Method.SessionPrompt:
91
- return this.onSessionPrompt(params);
92
+ return this.onSessionPrompt(parseParams(SessionPromptParamsSchema, params));
92
93
  case Method.SessionSnapshot:
93
- return this.onSessionSnapshot(params);
94
+ return this.onSessionSnapshot(parseParams(SessionSnapshotParamsSchema, params));
94
95
  case Method.SessionRespond:
95
- return this.onSessionRespond(params);
96
+ return this.onSessionRespond(parseParams(SessionRespondParamsSchema, params));
96
97
  case Method.SessionSteer:
97
- return this.onSessionSteer(params);
98
+ return this.onSessionSteer(parseParams(SessionSteerParamsSchema, params));
98
99
  case Method.SessionFollowUp:
99
- return this.onSessionFollowUp(params);
100
+ return this.onSessionFollowUp(parseParams(SessionFollowUpParamsSchema, params));
100
101
  case Method.SessionCancel:
101
- return this.onSessionCancel(params);
102
+ return this.onSessionCancel(parseParams(SessionCancelParamsSchema, params));
102
103
  case Method.SessionInvoke:
103
- return this.onSessionInvoke(params);
104
+ return this.onSessionInvoke(parseParams(SessionInvokeParamsSchema, params));
104
105
  default:
105
106
  throw new RpcError(ErrorCode.MethodNotFound, `unknown method: ${method}`);
106
107
  }
107
108
  }
108
109
  // ── Handshake ─────────────────────────────────────────────────────────────
109
110
  onInitialize(params) {
110
- if (typeof params?.protocolVersion !== "number") {
111
- throw new RpcError(ErrorCode.InvalidParams, "protocolVersion (number) required");
112
- }
113
111
  this.clientApproval = params.clientCapabilities?.approval ?? false;
114
112
  this.clientQuestion = params.clientCapabilities?.question ?? false;
115
113
  this.initialized = true;
@@ -132,19 +130,19 @@ export class AppServer {
132
130
  return { sessionId: session.id, workDir: session.workDir };
133
131
  }
134
132
  async onSessionResume(params) {
135
- const session = await this.harness.resumeSession(this.requireId(params));
133
+ const session = await this.harness.resumeSession(params.sessionId);
136
134
  this.wireSession(session);
137
135
  return { sessionId: session.id, workDir: session.workDir };
138
136
  }
139
137
  async onSessionFork(params) {
140
- const session = await this.harness.forkSession(this.requireId(params), {
138
+ const session = await this.harness.forkSession(params.sessionId, {
141
139
  ...(params.title !== undefined ? { title: params.title } : {}),
142
140
  });
143
141
  this.wireSession(session);
144
142
  return { sessionId: session.id, workDir: session.workDir };
145
143
  }
146
144
  async onSessionClose(params) {
147
- const id = this.requireId(params);
145
+ const id = params.sessionId;
148
146
  this.wired.get(id)?.unsubscribe();
149
147
  this.wired.delete(id);
150
148
  this.rejectReverseForSession(id, "session closed");
@@ -176,12 +174,12 @@ export class AppServer {
176
174
  }
177
175
  onSessionSteer(params) {
178
176
  const session = this.requireSession(params);
179
- return { accepted: true, steerId: session.steer(params.text) };
177
+ return { accepted: true, steerId: session.steer(params.text).steerId };
180
178
  }
181
179
  onSessionFollowUp(params) {
182
180
  const session = this.requireSession(params);
183
- const steerId = session.followUp(params.text);
184
- return steerId === null ? { accepted: false } : { accepted: true, steerId };
181
+ const receipt = session.followUp(params.text);
182
+ return receipt === null ? { accepted: false } : { accepted: true, steerId: receipt.steerId };
185
183
  }
186
184
  onSessionCancel(params) {
187
185
  const session = this.requireSession(params);
@@ -192,15 +190,34 @@ export class AppServer {
192
190
  }
193
191
  async onSessionInvoke(params) {
194
192
  const session = this.requireSession(params);
195
- if (typeof params.method !== "string" || !INVOKABLE.has(params.method)) {
196
- throw new RpcError(ErrorCode.MethodNotFound, `not invokable: ${String(params.method)}`);
193
+ if (!INVOKABLE.has(params.method)) {
194
+ throw new RpcError(ErrorCode.MethodNotFound, `not invokable: ${params.method}`);
195
+ }
196
+ // `"<service>.<op>"` resolves through the session's capability accessor; a bare name is a
197
+ // method on the session. Reading a REQUIRE accessor throws when that capability is not open,
198
+ // which is a legitimate answer for a remote caller — reported as "unavailable", not a crash.
199
+ const dot = params.method.indexOf(".");
200
+ let target = session;
201
+ let name = params.method;
202
+ if (dot > 0) {
203
+ const service = params.method.slice(0, dot);
204
+ name = params.method.slice(dot + 1);
205
+ try {
206
+ target = session[service];
207
+ }
208
+ catch (error) {
209
+ throw new RpcError(ErrorCode.MethodNotFound, `capability unavailable: ${service} (${error instanceof Error ? error.message : String(error)})`);
210
+ }
211
+ if (target === undefined || target === null) {
212
+ throw new RpcError(ErrorCode.MethodNotFound, `capability unavailable: ${service}`);
213
+ }
197
214
  }
198
- const fn = session[params.method];
215
+ const fn = target[name];
199
216
  if (typeof fn !== "function") {
200
217
  throw new RpcError(ErrorCode.MethodNotFound, `not a method: ${params.method}`);
201
218
  }
202
- const args = Array.isArray(params.args) ? params.args : [];
203
- const value = await fn.apply(session, args);
219
+ const args = [...(params.args ?? [])];
220
+ const value = await fn.apply(target, args);
204
221
  return { value: toWireSafe(value) };
205
222
  }
206
223
  // ── Per-session wiring: events out, reverse-RPC for approvals/questions ──────
@@ -258,16 +275,10 @@ export class AppServer {
258
275
  }
259
276
  }
260
277
  // ── Helpers ──────────────────────────────────────────────────────────────────
261
- requireId(ref) {
262
- if (!ref || typeof ref.sessionId !== "string")
263
- throw new RpcError(ErrorCode.InvalidParams, "sessionId required");
264
- return ref.sessionId;
265
- }
266
278
  requireSession(ref) {
267
- const id = this.requireId(ref);
268
- const session = this.harness.getSession(id);
279
+ const session = this.harness.getSession(ref.sessionId);
269
280
  if (!session)
270
- throw new RpcError(ErrorCode.SessionNotFound, `session not found: ${id}`);
281
+ throw new RpcError(ErrorCode.SessionNotFound, `session not found: ${ref.sessionId}`);
271
282
  return session;
272
283
  }
273
284
  }
package/dist/core.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * `import { Runner, defineAgent, defineModel } from 'operon-agents/core'`
5
5
  *
6
6
  * Everything the core engine exposes: Runner / Session / Agent / capabilities /
7
- * machines / tools. Pass no machine and omit file tools to run a stateless,
8
- * no-filesystem agent (a `NullMachine` refuses any stray I/O). For the
7
+ * environments / tools. Pass no environment and omit file tools to run a stateless,
8
+ * no-filesystem agent (a `NullEnvironment` refuses any stray I/O). For the
9
9
  * batteries-included facade, import from `operon-agents` instead.
10
10
  */
11
11
  export * from "operon-agents-core";
package/dist/core.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * `import { Runner, defineAgent, defineModel } from 'operon-agents/core'`
5
5
  *
6
6
  * Everything the core engine exposes: Runner / Session / Agent / capabilities /
7
- * machines / tools. Pass no machine and omit file tools to run a stateless,
8
- * no-filesystem agent (a `NullMachine` refuses any stray I/O). For the
7
+ * environments / tools. Pass no environment and omit file tools to run a stateless,
8
+ * no-filesystem agent (a `NullEnvironment` refuses any stray I/O). For the
9
9
  * batteries-included facade, import from `operon-agents` instead.
10
10
  */
11
11
  export * from "operon-agents-core";
@@ -52,7 +52,7 @@ function parseField(field, min, max, name) {
52
52
  return out;
53
53
  }
54
54
  // Cron numeric fields are digit-only; `Number(...)` would otherwise accept '', '1e1',
55
- // '0x10', '+5', ' 3 ' — none valid cron. Gate before conversion to machine typos.
55
+ // '0x10', '+5', ' 3 ' — none valid cron. Gate before conversion to surface typos.
56
56
  const DIGIT_ONLY = /^\d+$/;
57
57
  function parseCronInt(raw, name, role) {
58
58
  if (!DIGIT_ONLY.test(raw)) {
@@ -29,8 +29,8 @@ export function normalizeCronTaskInput(manager, input) {
29
29
  export function cronExtension(options = {}) {
30
30
  return {
31
31
  id: "cron",
32
- async setup(api) {
33
- // One manager per session: setup runs per session open, so scheduling state is
32
+ async session(api) {
33
+ // One manager per session: session() runs per session open, so scheduling state is
34
34
  // session-scoped exactly as the capability's openSession was.
35
35
  const manager = new CronManager(options);
36
36
  manager.attach({