@nylorun/agents 0.0.0-bootstrap → 0.3.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/LICENSE +192 -0
- package/README.md +60 -0
- package/dist/client.d.ts +187 -0
- package/dist/client.js +169 -0
- package/dist/define.d.ts +1 -0
- package/dist/define.js +1 -0
- package/dist/execute-action.d.ts +4 -0
- package/dist/execute-action.js +190 -0
- package/dist/executor.d.ts +15 -0
- package/dist/executor.js +210 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +8 -0
- package/dist/plugins/launch.d.ts +17 -0
- package/dist/plugins/launch.js +71 -0
- package/dist/plugins/load.d.ts +23 -0
- package/dist/plugins/load.js +491 -0
- package/dist/plugins/plugin.d.ts +8 -0
- package/dist/plugins/plugin.js +45 -0
- package/dist/sse.d.ts +12 -0
- package/dist/sse.js +97 -0
- package/package.json +55 -7
- package/index.js +0 -1
- package/publish.out +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { implementationsFor, isToolError, normalizeToolDefinition, normalizedSchemasFor, } from "@nylorun/core/define";
|
|
2
|
+
class Suspend {
|
|
3
|
+
outcome;
|
|
4
|
+
constructor(outcome) {
|
|
5
|
+
this.outcome = outcome;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: {};
|
|
11
|
+
/** Executes code only after a host-issued claim. No engine dependency. */
|
|
12
|
+
export async function executeAction(action, agent, signal) {
|
|
13
|
+
const impl = implementationsFor(agent)[action.capabilityId];
|
|
14
|
+
if (!impl)
|
|
15
|
+
throw new Error(`No capability ${action.capabilityId}`);
|
|
16
|
+
if (action.kind === "beforeModelCall") {
|
|
17
|
+
if (!impl.beforeModelCall)
|
|
18
|
+
throw new Error("Missing beforeModelCall implementation");
|
|
19
|
+
try {
|
|
20
|
+
return { value: (await impl.beforeModelCall(action.input)) ?? {} };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { value: { block: message(error) } };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (action.kind === "afterModelCall") {
|
|
27
|
+
if (!impl.afterModelCall)
|
|
28
|
+
throw new Error("Missing afterModelCall implementation");
|
|
29
|
+
try {
|
|
30
|
+
return {
|
|
31
|
+
value: (await impl.afterModelCall(action.input, action.context)) ?? {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return { value: { block: message(error) } };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const raw = impl.tools?.[action.toolName ?? ""];
|
|
39
|
+
if (!raw)
|
|
40
|
+
throw new Error("Missing tool implementation");
|
|
41
|
+
const tool = normalizeToolDefinition(raw);
|
|
42
|
+
const schemas = normalizedSchemasFor(tool);
|
|
43
|
+
const input = schemas.inputSchema.validate(action.input);
|
|
44
|
+
if (!input.ok)
|
|
45
|
+
return {
|
|
46
|
+
value: {
|
|
47
|
+
kind: "failed",
|
|
48
|
+
code: "tool.invalid-input",
|
|
49
|
+
message: JSON.stringify(input.issues),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const ctx = action.context;
|
|
53
|
+
const state = { ...object(ctx.state) };
|
|
54
|
+
const statePatch = {};
|
|
55
|
+
const resume = object(ctx.resume);
|
|
56
|
+
const token = object(resume.token);
|
|
57
|
+
const memo = object(token._nylorun);
|
|
58
|
+
const steps = { ...object(memo.steps) };
|
|
59
|
+
const answers = { ...object(memo.answers) };
|
|
60
|
+
let waitIndex = 0;
|
|
61
|
+
let gateApproved = Boolean(memo.gateApproved) ||
|
|
62
|
+
Boolean(memo.approvalGate && resume.kind === "approval" && resume.approved);
|
|
63
|
+
const durableToken = (extra = {}) => ({
|
|
64
|
+
_nylorun: { steps, answers, waitIndex, gateApproved, ...extra },
|
|
65
|
+
});
|
|
66
|
+
const wait = async (kind, prompt, metadata) => {
|
|
67
|
+
const index = String(waitIndex++);
|
|
68
|
+
if (index in answers)
|
|
69
|
+
return answers[index];
|
|
70
|
+
if (!memo.approvalGate &&
|
|
71
|
+
resume.kind === kind &&
|
|
72
|
+
Number(memo.waitIndex ?? 0) === Number(index)) {
|
|
73
|
+
const answer = kind === "approval" ? Boolean(resume.approved) : resume.value;
|
|
74
|
+
answers[index] = answer;
|
|
75
|
+
return answer;
|
|
76
|
+
}
|
|
77
|
+
throw new Suspend({
|
|
78
|
+
kind: "interaction-required",
|
|
79
|
+
interaction: { kind, prompt, ...(metadata ? { metadata } : {}) },
|
|
80
|
+
token: {
|
|
81
|
+
_nylorun: { steps, answers, waitIndex: Number(index), gateApproved },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const context = {
|
|
86
|
+
executionId: String(ctx.executionId ?? action.sessionId),
|
|
87
|
+
turnId: String(ctx.turnId ?? action.turnId),
|
|
88
|
+
stepId: String(ctx.stepId ?? ""),
|
|
89
|
+
callId: String(ctx.callId ?? action.actionId),
|
|
90
|
+
invocationId: String(ctx.invocationId ?? action.actionId),
|
|
91
|
+
idempotencyKey: String(ctx.idempotencyKey ?? action.actionId),
|
|
92
|
+
signal,
|
|
93
|
+
info: ctx.info,
|
|
94
|
+
session: { id: action.sessionId },
|
|
95
|
+
...(ctx.resume ? { resume: ctx.resume } : {}),
|
|
96
|
+
state: {
|
|
97
|
+
get: (key) => state[key],
|
|
98
|
+
set: (key, value) => {
|
|
99
|
+
state[key] = value;
|
|
100
|
+
statePatch[key] = value;
|
|
101
|
+
},
|
|
102
|
+
entries: () => ({ ...state }),
|
|
103
|
+
},
|
|
104
|
+
progress() {
|
|
105
|
+
/* Canonical events are host-owned; progress transport is deferred. */
|
|
106
|
+
},
|
|
107
|
+
ask: (prompt, options) => wait("response", prompt, options),
|
|
108
|
+
approve: (prompt) => wait("approval", prompt),
|
|
109
|
+
sleep: async (duration) => {
|
|
110
|
+
throw new Suspend({
|
|
111
|
+
kind: "deferred",
|
|
112
|
+
token: durableToken({ wait: { kind: "sleep", duration } }),
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
waitFor: async (event, options) => {
|
|
116
|
+
throw new Suspend({
|
|
117
|
+
kind: "deferred",
|
|
118
|
+
token: durableToken({ wait: { kind: "waitFor", event, ...options } }),
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
step: async (name, fn) => {
|
|
122
|
+
if (Object.prototype.hasOwnProperty.call(steps, name))
|
|
123
|
+
return steps[name];
|
|
124
|
+
signal.throwIfAborted();
|
|
125
|
+
const value = await fn();
|
|
126
|
+
// Memoized values are durable only with a persisted action outcome. Interrupted actions stay uncertain.
|
|
127
|
+
steps[name] = JSON.parse(JSON.stringify(value ?? null));
|
|
128
|
+
return value;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
signal.throwIfAborted();
|
|
133
|
+
if (tool.approval && !gateApproved) {
|
|
134
|
+
const approval = await tool.approval(input.value);
|
|
135
|
+
if (approval)
|
|
136
|
+
throw new Suspend({
|
|
137
|
+
kind: "interaction-required",
|
|
138
|
+
interaction: {
|
|
139
|
+
kind: "approval",
|
|
140
|
+
prompt: typeof approval === "string" ? approval : `Approve ${tool.name}?`,
|
|
141
|
+
},
|
|
142
|
+
token: durableToken({ approvalGate: true }),
|
|
143
|
+
});
|
|
144
|
+
gateApproved = true;
|
|
145
|
+
}
|
|
146
|
+
if (resume.kind === "approval" && resume.approved === false)
|
|
147
|
+
return { value: { kind: "denied", reason: "Approval denied" } };
|
|
148
|
+
const value = await tool.execute(input.value, context);
|
|
149
|
+
const tagged = object(value);
|
|
150
|
+
let outcome;
|
|
151
|
+
if ([
|
|
152
|
+
"completed",
|
|
153
|
+
"failed",
|
|
154
|
+
"denied",
|
|
155
|
+
"interaction-required",
|
|
156
|
+
"deferred",
|
|
157
|
+
].includes(tagged.kind))
|
|
158
|
+
outcome = value;
|
|
159
|
+
else
|
|
160
|
+
outcome = { kind: "completed", output: value ?? null };
|
|
161
|
+
if (outcome.kind === "completed" && schemas.outputSchema) {
|
|
162
|
+
const parsed = schemas.outputSchema.validate(outcome.output);
|
|
163
|
+
outcome = parsed.ok
|
|
164
|
+
? { ...outcome, output: parsed.value }
|
|
165
|
+
: {
|
|
166
|
+
kind: "failed",
|
|
167
|
+
code: "tool.invalid-output",
|
|
168
|
+
message: JSON.stringify(parsed.issues),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return { value: outcome, statePatch };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (signal.aborted)
|
|
175
|
+
throw signal.reason;
|
|
176
|
+
return {
|
|
177
|
+
value: error instanceof Suspend
|
|
178
|
+
? error.outcome
|
|
179
|
+
: {
|
|
180
|
+
kind: "failed",
|
|
181
|
+
code: isToolError(error) ? error.code : "tool.execution-failed",
|
|
182
|
+
message: message(error),
|
|
183
|
+
},
|
|
184
|
+
statePatch,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function message(error) {
|
|
189
|
+
return error instanceof Error ? error.message : String(error);
|
|
190
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AgentSource } from "./client.js";
|
|
2
|
+
import { type Destination } from "./http.js";
|
|
3
|
+
export interface ConnectOptions {
|
|
4
|
+
agents: readonly AgentSource[];
|
|
5
|
+
runtime?: Destination;
|
|
6
|
+
implementationVersion?: string;
|
|
7
|
+
onError?: (error: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface AgentConnection {
|
|
10
|
+
/** Settles after authenticated SSE is established and initial discovery succeeds. */
|
|
11
|
+
readonly ready: Promise<void>;
|
|
12
|
+
/** Abort subscriptions and leases. Running user code receives an AbortSignal. */
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function connectAgents(options: ConnectOptions): AgentConnection;
|
package/dist/executor.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { AgentManifestSchema, ActionSchema, ActionClaimResponseSchema, } from "@nylorun/core/contracts";
|
|
2
|
+
import { assertNoMiddlewareClosures } from "./client.js";
|
|
3
|
+
import { Transport, RuntimeError, delay, env, id, segment, } from "./http.js";
|
|
4
|
+
import { readSSE } from "./sse.js";
|
|
5
|
+
import { executeAction } from "./execute-action.js";
|
|
6
|
+
export function connectAgents(options) {
|
|
7
|
+
const transport = new Transport(options.runtime, "executor");
|
|
8
|
+
const version = options.implementationVersion ??
|
|
9
|
+
env("NYLORUN_IMPLEMENTATION_VERSION") ??
|
|
10
|
+
"dev";
|
|
11
|
+
const agents = new Map();
|
|
12
|
+
for (const source of options.agents) {
|
|
13
|
+
const built = source.build?.() ?? source;
|
|
14
|
+
assertNoMiddlewareClosures(built);
|
|
15
|
+
AgentManifestSchema.parse(built.manifest);
|
|
16
|
+
if (agents.has(built.id))
|
|
17
|
+
throw new Error(`Duplicate connected agent ${built.id}`);
|
|
18
|
+
agents.set(built.id, built);
|
|
19
|
+
}
|
|
20
|
+
if (!agents.size)
|
|
21
|
+
throw new Error("connectAgents requires at least one agent");
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const signal = controller.signal;
|
|
24
|
+
const active = new Map();
|
|
25
|
+
let resolveReady;
|
|
26
|
+
let rejectReady;
|
|
27
|
+
const ready = new Promise((resolve, reject) => {
|
|
28
|
+
resolveReady = resolve;
|
|
29
|
+
rejectReady = reject;
|
|
30
|
+
});
|
|
31
|
+
// Avoid an unhandled rejection for callers that immediately close the handle.
|
|
32
|
+
void ready.catch(() => { });
|
|
33
|
+
const report = (error) => {
|
|
34
|
+
try {
|
|
35
|
+
options.onError?.(error);
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
};
|
|
39
|
+
let discovering = false, dirty = false;
|
|
40
|
+
const discover = async () => {
|
|
41
|
+
dirty = true;
|
|
42
|
+
if (discovering)
|
|
43
|
+
return;
|
|
44
|
+
discovering = true;
|
|
45
|
+
try {
|
|
46
|
+
while (dirty && !signal.aborted) {
|
|
47
|
+
dirty = false;
|
|
48
|
+
const response = await transport.json("/v1/actions", "GET", undefined, signal);
|
|
49
|
+
for (const item of response.actions) {
|
|
50
|
+
const action = ActionSchema.parse(item);
|
|
51
|
+
const agent = agents.get(action.agentId);
|
|
52
|
+
if (!agent || action.status !== "pending" || active.has(action.actionId))
|
|
53
|
+
continue;
|
|
54
|
+
const work = new AbortController();
|
|
55
|
+
active.set(action.actionId, work);
|
|
56
|
+
let delivered = false;
|
|
57
|
+
void processAction(action, agent, work)
|
|
58
|
+
.then(() => {
|
|
59
|
+
delivered = true;
|
|
60
|
+
})
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
if (!signal.aborted)
|
|
63
|
+
report(error);
|
|
64
|
+
})
|
|
65
|
+
.finally(() => {
|
|
66
|
+
active.delete(action.actionId);
|
|
67
|
+
// Completion-driven discovery drains bounded server pages without periodic polling.
|
|
68
|
+
if (delivered && !signal.aborted)
|
|
69
|
+
void discover().catch(report);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
discovering = false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const processAction = async (action, agent, work) => {
|
|
79
|
+
const stop = () => work.abort(signal.reason);
|
|
80
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
81
|
+
let renewal;
|
|
82
|
+
let renewing = true;
|
|
83
|
+
let wakeRenewal;
|
|
84
|
+
try {
|
|
85
|
+
const claim = ActionClaimResponseSchema.parse(await transport.json(`/v1/actions/${segment(action.actionId)}/claim`, "POST", {
|
|
86
|
+
requestId: id(),
|
|
87
|
+
implementationVersion: version,
|
|
88
|
+
}, work.signal));
|
|
89
|
+
if (claim.action.actionId !== action.actionId)
|
|
90
|
+
throw new Error("Claim definition mismatch");
|
|
91
|
+
let expires = Date.parse(claim.leaseExpiresAt);
|
|
92
|
+
renewal = (async () => {
|
|
93
|
+
while (renewing && !work.signal.aborted) {
|
|
94
|
+
const remaining = expires - Date.now();
|
|
95
|
+
if (!Number.isFinite(remaining) || remaining <= 0)
|
|
96
|
+
throw new Error("Executor lease expired");
|
|
97
|
+
await new Promise((resolve) => {
|
|
98
|
+
const finish = () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
work.signal.removeEventListener("abort", finish);
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
const timer = setTimeout(finish, Math.max(50, Math.floor(remaining / 3)));
|
|
104
|
+
wakeRenewal = finish;
|
|
105
|
+
work.signal.addEventListener("abort", finish, { once: true });
|
|
106
|
+
});
|
|
107
|
+
if (!renewing || work.signal.aborted)
|
|
108
|
+
return;
|
|
109
|
+
const renewed = await transport.json(`/v1/actions/${segment(action.actionId)}/heartbeat`, "POST", {
|
|
110
|
+
requestId: id(),
|
|
111
|
+
claimId: claim.claimId,
|
|
112
|
+
generation: claim.generation,
|
|
113
|
+
}, work.signal);
|
|
114
|
+
expires = Date.parse(renewed.leaseExpiresAt);
|
|
115
|
+
}
|
|
116
|
+
})().catch((error) => {
|
|
117
|
+
if (renewing && !work.signal.aborted) {
|
|
118
|
+
work.abort(error);
|
|
119
|
+
report(error);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const outcome = await executeAction(claim.action, agent, work.signal);
|
|
123
|
+
work.signal.throwIfAborted();
|
|
124
|
+
const command = {
|
|
125
|
+
type: "action_result",
|
|
126
|
+
requestId: id(),
|
|
127
|
+
idempotencyKey: `action:${action.actionId}:${claim.generation}`,
|
|
128
|
+
actionId: action.actionId,
|
|
129
|
+
claimId: claim.claimId,
|
|
130
|
+
generation: claim.generation,
|
|
131
|
+
outcome,
|
|
132
|
+
};
|
|
133
|
+
// Retrying delivery never re-invokes the tool/hook. Lost acknowledgements use the same receipt key.
|
|
134
|
+
for (let attempt = 0;; attempt++) {
|
|
135
|
+
try {
|
|
136
|
+
await transport.json(`/v1/sessions/${segment(action.sessionId)}/commands`, "POST", command, work.signal);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (work.signal.aborted ||
|
|
141
|
+
attempt >= 4 ||
|
|
142
|
+
(error instanceof RuntimeError &&
|
|
143
|
+
error.status < 500 &&
|
|
144
|
+
![408, 429].includes(error.status)))
|
|
145
|
+
throw error;
|
|
146
|
+
await delay(Math.min(4000, 250 * 2 ** attempt), work.signal);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
renewing = false;
|
|
152
|
+
wakeRenewal?.();
|
|
153
|
+
await renewal;
|
|
154
|
+
signal.removeEventListener("abort", stop);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const loop = (async () => {
|
|
158
|
+
let retry = 250;
|
|
159
|
+
while (!signal.aborted) {
|
|
160
|
+
const subscription = new AbortController();
|
|
161
|
+
const stop = () => subscription.abort(signal.reason);
|
|
162
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
163
|
+
try {
|
|
164
|
+
const response = await transport.request("/v1/executors/connect", {
|
|
165
|
+
signal: subscription.signal,
|
|
166
|
+
headers: { Accept: "text/event-stream" },
|
|
167
|
+
});
|
|
168
|
+
if (!response.headers.get("content-type")?.includes("text/event-stream"))
|
|
169
|
+
throw new Error("Executor endpoint did not return SSE");
|
|
170
|
+
// Subscription exists before initial discovery; notifications can safely accumulate in its stream.
|
|
171
|
+
await discover();
|
|
172
|
+
resolveReady();
|
|
173
|
+
for await (const frame of readSSE(response, subscription.signal)) {
|
|
174
|
+
retry = 250;
|
|
175
|
+
if (frame.event === "work_available" ||
|
|
176
|
+
(frame.data && JSON.parse(frame.data).type === "work_available"))
|
|
177
|
+
await discover();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (signal.aborted)
|
|
182
|
+
break;
|
|
183
|
+
report(error);
|
|
184
|
+
if (error instanceof RuntimeError &&
|
|
185
|
+
[400, 401, 403, 404].includes(error.status)) {
|
|
186
|
+
rejectReady(error);
|
|
187
|
+
controller.abort(error);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
subscription.abort();
|
|
193
|
+
signal.removeEventListener("abort", stop);
|
|
194
|
+
}
|
|
195
|
+
if (!signal.aborted)
|
|
196
|
+
await delay(retry, signal).catch(() => { });
|
|
197
|
+
retry = Math.min(30000, retry * 2);
|
|
198
|
+
}
|
|
199
|
+
rejectReady(signal.reason ?? new Error("Executor connection closed"));
|
|
200
|
+
})();
|
|
201
|
+
return {
|
|
202
|
+
ready,
|
|
203
|
+
async close() {
|
|
204
|
+
controller.abort(new Error("Executor connection closed"));
|
|
205
|
+
for (const work of active.values())
|
|
206
|
+
work.abort(signal.reason);
|
|
207
|
+
await loop;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Destination {
|
|
2
|
+
url?: string;
|
|
3
|
+
key?: string;
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
}
|
|
6
|
+
export declare function env(name: string): string | undefined;
|
|
7
|
+
export declare class RuntimeError extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly body: unknown;
|
|
10
|
+
constructor(status: number, body: unknown);
|
|
11
|
+
}
|
|
12
|
+
export declare class Transport {
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly fetcher: typeof fetch;
|
|
16
|
+
constructor(options?: Destination, role?: "server" | "executor");
|
|
17
|
+
request(path: string, init?: RequestInit): Promise<Response>;
|
|
18
|
+
json<T>(path: string, method?: string, body?: unknown, signal?: AbortSignal): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
export declare const id: () => `${string}-${string}-${string}-${string}-${string}`;
|
|
21
|
+
export declare const segment: (value: string) => string;
|
|
22
|
+
export declare function delay(ms: number, signal: AbortSignal): Promise<void>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function env(name) {
|
|
2
|
+
return typeof process !== "undefined" ? process.env[name] : undefined;
|
|
3
|
+
}
|
|
4
|
+
export class RuntimeError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, body) {
|
|
8
|
+
super(`Runtime HTTP ${status}: ${JSON.stringify(body)}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class Transport {
|
|
14
|
+
url;
|
|
15
|
+
key;
|
|
16
|
+
fetcher;
|
|
17
|
+
constructor(options = {}, role = "server") {
|
|
18
|
+
const url = options.url ?? env("NYLORUN_RUNTIME_URL");
|
|
19
|
+
const key = options.key ??
|
|
20
|
+
env(role === "server" ? "NYLORUN_SERVER_KEY" : "NYLORUN_EXECUTOR_KEY");
|
|
21
|
+
if (!url || !key)
|
|
22
|
+
throw new Error(`Set Runtime url and ${role} key explicitly or via NYLORUN_RUNTIME_URL / NYLORUN_${role.toUpperCase()}_KEY`);
|
|
23
|
+
const parsed = new URL(url);
|
|
24
|
+
if (!["http:", "https:"].includes(parsed.protocol))
|
|
25
|
+
throw new Error("Runtime requires an HTTP(S) URL");
|
|
26
|
+
this.url = url.replace(/\/$/, "");
|
|
27
|
+
this.key = key;
|
|
28
|
+
this.fetcher = options.fetch ?? globalThis.fetch;
|
|
29
|
+
}
|
|
30
|
+
async request(path, init = {}) {
|
|
31
|
+
const headers = new Headers(init.headers);
|
|
32
|
+
headers.set("Authorization", `Bearer ${this.key}`);
|
|
33
|
+
if (init.body)
|
|
34
|
+
headers.set("Content-Type", "application/json");
|
|
35
|
+
const response = await this.fetcher(this.url + path, {
|
|
36
|
+
...init,
|
|
37
|
+
headers,
|
|
38
|
+
redirect: "error",
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
let body = text;
|
|
43
|
+
try {
|
|
44
|
+
body = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
throw new RuntimeError(response.status, body);
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
async json(path, method = "GET", body, signal) {
|
|
52
|
+
const response = await this.request(path, {
|
|
53
|
+
method,
|
|
54
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
55
|
+
signal,
|
|
56
|
+
});
|
|
57
|
+
return response.status === 204
|
|
58
|
+
? undefined
|
|
59
|
+
: response.json();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const id = () => globalThis.crypto.randomUUID();
|
|
63
|
+
export const segment = (value) => encodeURIComponent(value);
|
|
64
|
+
export function delay(ms, signal) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
if (signal.aborted) {
|
|
67
|
+
reject(signal.reason);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const stop = () => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
signal.removeEventListener("abort", stop);
|
|
73
|
+
reject(signal.reason);
|
|
74
|
+
};
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
signal.removeEventListener("abort", stop);
|
|
77
|
+
resolve();
|
|
78
|
+
}, ms);
|
|
79
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
80
|
+
});
|
|
81
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, capability, tool, defineSchema, ToolError, } from "@nylorun/core/define";
|
|
2
|
+
export type { AgentOptions, AgentManifest, BuiltAgent, ToolDefinition, ToolExecutionContext, ToolOutcome, Patch, Decision, JsonValue, JsonObject, } from "@nylorun/core/define";
|
|
3
|
+
export type { LiveEvent, SessionCommand, Action, ActionOutcome, CredentialInfo, CredentialSelection, VaultInfo, } from "@nylorun/core/contracts";
|
|
4
|
+
export { AgentsClient, SessionClient, createClient } from "./client.js";
|
|
5
|
+
export type { AgentSource, SessionView, CommandOptions } from "./client.js";
|
|
6
|
+
export { connectAgents } from "./executor.js";
|
|
7
|
+
export type { ConnectOptions, AgentConnection } from "./executor.js";
|
|
8
|
+
export { RuntimeError } from "./http.js";
|
|
9
|
+
export type { Destination } from "./http.js";
|
|
10
|
+
export { plugin } from "./plugins/plugin.js";
|
|
11
|
+
export type { PluginCapability } from "./plugins/plugin.js";
|
|
12
|
+
export { loadPlugin, PluginError } from "./plugins/load.js";
|
|
13
|
+
export type { LoadedPlugin, PluginDiagnostic } from "./plugins/load.js";
|
|
14
|
+
export { prepareStdioLaunch, expandPluginPlaceholders } from "./plugins/launch.js";
|
|
15
|
+
export type { StdioLaunch } from "./plugins/launch.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Definition authoring and wire contracts are the only harness runtime imports.
|
|
2
|
+
export { Agent, AgentBuilder, AgentBuildError, AgentLifecycleError, capability, tool, defineSchema, ToolError, } from "@nylorun/core/define";
|
|
3
|
+
export { AgentsClient, SessionClient, createClient } from "./client.js";
|
|
4
|
+
export { connectAgents } from "./executor.js";
|
|
5
|
+
export { RuntimeError } from "./http.js";
|
|
6
|
+
export { plugin } from "./plugins/plugin.js";
|
|
7
|
+
export { loadPlugin, PluginError } from "./plugins/load.js";
|
|
8
|
+
export { prepareStdioLaunch, expandPluginPlaceholders } from "./plugins/launch.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { McpServerManifest } from "@nylorun/core/define";
|
|
2
|
+
/** Single-pass replacement. Text inserted for a token is not scanned again. */
|
|
3
|
+
export declare function expandPluginPlaceholders(value: string, pluginRoot: string, pluginData: string): string;
|
|
4
|
+
export interface StdioLaunch {
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly args: readonly string[];
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly env: Readonly<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
export declare function prepareStdioLaunch(server: Extract<McpServerManifest, {
|
|
11
|
+
type: "stdio";
|
|
12
|
+
}>, options: {
|
|
13
|
+
readonly pluginRoot: string;
|
|
14
|
+
readonly pluginData: string;
|
|
15
|
+
readonly baseEnv?: Readonly<Record<string, string | undefined>>;
|
|
16
|
+
}): StdioLaunch;
|
|
17
|
+
export declare function resolveContainedCwd(cwd: string, pluginRoot: string, pluginData: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
const ROOT_TOKEN = "${PLUGIN_ROOT}";
|
|
4
|
+
const DATA_TOKEN = "${PLUGIN_DATA}";
|
|
5
|
+
/** Single-pass replacement. Text inserted for a token is not scanned again. */
|
|
6
|
+
export function expandPluginPlaceholders(value, pluginRoot, pluginData) {
|
|
7
|
+
let out = "";
|
|
8
|
+
for (let index = 0; index < value.length;) {
|
|
9
|
+
if (value.startsWith(ROOT_TOKEN, index)) {
|
|
10
|
+
out += pluginRoot;
|
|
11
|
+
index += ROOT_TOKEN.length;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (value.startsWith(DATA_TOKEN, index)) {
|
|
15
|
+
out += pluginData;
|
|
16
|
+
index += DATA_TOKEN.length;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
out += value[index];
|
|
20
|
+
index += 1;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
export function prepareStdioLaunch(server, options) {
|
|
25
|
+
const pluginRoot = options.pluginRoot;
|
|
26
|
+
const pluginData = options.pluginData;
|
|
27
|
+
mkdirSync(pluginData, { recursive: true });
|
|
28
|
+
const command = resolveCommand(server.command, pluginRoot);
|
|
29
|
+
const args = (server.args ?? []).map((arg) => expandPluginPlaceholders(arg, pluginRoot, pluginData));
|
|
30
|
+
const cwd = server.cwd
|
|
31
|
+
? resolveContainedCwd(server.cwd, pluginRoot, pluginData)
|
|
32
|
+
: pluginRoot;
|
|
33
|
+
const env = {};
|
|
34
|
+
for (const [key, value] of Object.entries(options.baseEnv ?? {})) {
|
|
35
|
+
if (typeof value === "string")
|
|
36
|
+
setEnv(env, key, value);
|
|
37
|
+
}
|
|
38
|
+
for (const [key, value] of Object.entries(server.env ?? {}))
|
|
39
|
+
setEnv(env, key, expandPluginPlaceholders(value, pluginRoot, pluginData));
|
|
40
|
+
setEnv(env, "PLUGIN_ROOT", pluginRoot);
|
|
41
|
+
setEnv(env, "PLUGIN_DATA", pluginData);
|
|
42
|
+
return { command, args, cwd, env };
|
|
43
|
+
}
|
|
44
|
+
function resolveCommand(command, pluginRoot) {
|
|
45
|
+
if (command.startsWith("./"))
|
|
46
|
+
return resolve(pluginRoot, command);
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
export function resolveContainedCwd(cwd, pluginRoot, pluginData) {
|
|
50
|
+
const expanded = expandPluginPlaceholders(cwd, pluginRoot, pluginData);
|
|
51
|
+
const absolute = isAbsolute(expanded) ? expanded : resolve(pluginRoot, expanded);
|
|
52
|
+
const root = cwdTargetsData(cwd) ? pluginData : pluginRoot;
|
|
53
|
+
if (!isInside(root, absolute))
|
|
54
|
+
throw new Error(`Working directory escapes ${root}`);
|
|
55
|
+
return absolute;
|
|
56
|
+
}
|
|
57
|
+
function cwdTargetsData(cwd) {
|
|
58
|
+
return cwd === DATA_TOKEN || cwd.startsWith(`${DATA_TOKEN}/`);
|
|
59
|
+
}
|
|
60
|
+
function isInside(root, target) {
|
|
61
|
+
const rel = relative(root, target);
|
|
62
|
+
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
63
|
+
}
|
|
64
|
+
function setEnv(env, key, value) {
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
const existing = Object.keys(env).find((name) => name.toLowerCase() === key.toLowerCase());
|
|
67
|
+
if (existing && existing !== key)
|
|
68
|
+
delete env[existing];
|
|
69
|
+
}
|
|
70
|
+
env[key] = value;
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { McpServerManifest, SkillRecord } from "@nylorun/core/define";
|
|
2
|
+
export declare const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
3
|
+
export declare const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
4
|
+
export interface PluginDiagnostic {
|
|
5
|
+
readonly severity: "info" | "warning";
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly message: string;
|
|
8
|
+
readonly path?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class PluginError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly diagnostics: readonly PluginDiagnostic[];
|
|
13
|
+
constructor(code: string, message: string, diagnostics?: readonly PluginDiagnostic[]);
|
|
14
|
+
}
|
|
15
|
+
export interface LoadedPlugin {
|
|
16
|
+
readonly root: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly description?: string;
|
|
19
|
+
readonly skills: Readonly<Record<string, SkillRecord>>;
|
|
20
|
+
readonly mcpServers: Readonly<Record<string, McpServerManifest>>;
|
|
21
|
+
readonly diagnostics: readonly PluginDiagnostic[];
|
|
22
|
+
}
|
|
23
|
+
export declare function loadPlugin(directory: string): LoadedPlugin;
|