runline 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/engine.d.ts +29 -1
- package/dist/core/engine.js +32 -2
- package/dist/index.d.ts +1 -1
- package/dist/plugins/shiftAtlas/src/graph.js +324 -0
- package/dist/plugins/shiftAtlas/src/index.js +22 -0
- package/dist/plugins/shiftAtlas/src/shared.js +28 -0
- package/dist/plugins/shiftOcr/src/index.js +153 -0
- package/dist/sdk.d.ts +11 -1
- package/dist/sdk.js +6 -2
- package/package.json +2 -2
package/dist/core/engine.d.ts
CHANGED
|
@@ -9,6 +9,32 @@ export interface EngineOptions {
|
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
memoryLimitBytes?: number;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* One resolved action call, observed at the engine's dispatch point.
|
|
14
|
+
*
|
|
15
|
+
* Fired only around `action.execute()` — an unknown path or a failed
|
|
16
|
+
* input validation throws before plugin code runs and is not an
|
|
17
|
+
* invocation. `error` is present exactly when execute rejected: the
|
|
18
|
+
* thrown value as-is, except a nullish throw, which is substituted
|
|
19
|
+
* with an Error so failure is never mistaken for success. The hook
|
|
20
|
+
* sees it in the host process even though the worker only ever
|
|
21
|
+
* receives the message string.
|
|
22
|
+
*/
|
|
23
|
+
export interface ActionInvocation {
|
|
24
|
+
plugin: string;
|
|
25
|
+
action: string;
|
|
26
|
+
/** The full callable path, "plugin.action". */
|
|
27
|
+
path: string;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
error?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export interface EngineHooks {
|
|
32
|
+
/**
|
|
33
|
+
* Observer for every action invocation. Must not throw — but if it
|
|
34
|
+
* does, the engine swallows it: observability never breaks a run.
|
|
35
|
+
*/
|
|
36
|
+
onAction?: (info: ActionInvocation) => void;
|
|
37
|
+
}
|
|
12
38
|
/**
|
|
13
39
|
* Whether to arm the host-side RSS watchdog for a worker run.
|
|
14
40
|
*
|
|
@@ -48,9 +74,10 @@ export declare function shouldArmRssWatchdog(versions?: Partial<Record<string, s
|
|
|
48
74
|
export declare class ExecutionEngine {
|
|
49
75
|
private registry;
|
|
50
76
|
private config;
|
|
77
|
+
private hooks;
|
|
51
78
|
private pooled;
|
|
52
79
|
private readonly maxRunsPerWorker;
|
|
53
|
-
constructor(registry: PluginRegistry, config: RunlineConfig);
|
|
80
|
+
constructor(registry: PluginRegistry, config: RunlineConfig, hooks?: EngineHooks);
|
|
54
81
|
/** Retire the pooled worker, if any. Safe to call repeatedly. */
|
|
55
82
|
dispose(): void;
|
|
56
83
|
/**
|
|
@@ -71,6 +98,7 @@ export declare class ExecutionEngine {
|
|
|
71
98
|
private release;
|
|
72
99
|
execute(code: string, options?: EngineOptions): Promise<ExecuteResult>;
|
|
73
100
|
private invokeAction;
|
|
101
|
+
private fireOnAction;
|
|
74
102
|
/**
|
|
75
103
|
* A miss on "salesforce.status" is often a plugin that registered
|
|
76
104
|
* its action name with the plugin prefix ("salesforce.status" inside
|
package/dist/core/engine.js
CHANGED
|
@@ -81,11 +81,13 @@ const RSS_WATCHDOG_SLACK_BYTES = 128 * 1024 * 1024;
|
|
|
81
81
|
export class ExecutionEngine {
|
|
82
82
|
registry;
|
|
83
83
|
config;
|
|
84
|
+
hooks;
|
|
84
85
|
pooled = null;
|
|
85
86
|
maxRunsPerWorker;
|
|
86
|
-
constructor(registry, config) {
|
|
87
|
+
constructor(registry, config, hooks = {}) {
|
|
87
88
|
this.registry = registry;
|
|
88
89
|
this.config = config;
|
|
90
|
+
this.hooks = hooks;
|
|
89
91
|
this.maxRunsPerWorker =
|
|
90
92
|
config.maxRunsPerWorker ?? DEFAULT_MAX_RUNS_PER_WORKER;
|
|
91
93
|
}
|
|
@@ -320,7 +322,35 @@ export class ExecutionEngine {
|
|
|
320
322
|
throw new Error(formatValidationError(path, validation));
|
|
321
323
|
}
|
|
322
324
|
}
|
|
323
|
-
|
|
325
|
+
const started = performance.now();
|
|
326
|
+
try {
|
|
327
|
+
const value = await action.execute(args, ctx);
|
|
328
|
+
this.fireOnAction(plugin.name, action.name, path, started, false);
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
// `failed` is a flag, not `error !== undefined` — a plugin that
|
|
333
|
+
// does `throw undefined` still failed.
|
|
334
|
+
this.fireOnAction(plugin.name, action.name, path, started, true, err);
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
fireOnAction(pluginName, actionName, path, started, failed, error) {
|
|
339
|
+
const onAction = this.hooks.onAction;
|
|
340
|
+
if (!onAction)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
onAction({
|
|
344
|
+
plugin: pluginName,
|
|
345
|
+
action: actionName,
|
|
346
|
+
path,
|
|
347
|
+
durationMs: performance.now() - started,
|
|
348
|
+
...(failed ? { error: error ?? new Error("Unknown error") } : {}),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Observability never breaks a run.
|
|
353
|
+
}
|
|
324
354
|
}
|
|
325
355
|
/**
|
|
326
356
|
* A miss on "salesforce.status" is often a plugin that registered
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { addConnection, findConfigDir, getConnection, loadConfig, removeConnection, saveConfig, updateConnectionConfig, } from "./config/loader.js";
|
|
2
2
|
export type { RunlineConfig } from "./config/types.js";
|
|
3
3
|
export { DEFAULT_CONFIG } from "./config/types.js";
|
|
4
|
-
export type { EngineOptions, ExecuteResult } from "./core/engine.js";
|
|
4
|
+
export type { ActionInvocation, EngineHooks, EngineOptions, ExecuteResult, } from "./core/engine.js";
|
|
5
5
|
export { ExecutionEngine } from "./core/engine.js";
|
|
6
6
|
export type { BuildAuthUrlOptions, ExchangeCodeOptions, OAuthTokens, PKCEPair, RunOAuthOptions, } from "./core/oauth.js";
|
|
7
7
|
export { buildAuthUrl, exchangeAuthCode, generatePKCE, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_URI, runOAuth, } from "./core/oauth.js";
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { ATLAS_BASE, cursorSchema, enumSchema, GRAPH_ACTOR_KIND, GRAPH_ASSIGNMENT_ROLE, GRAPH_BINDING_EXECUTOR, GRAPH_BINDING_STATUS, GRAPH_CHANGE_ENTITY_TYPE, GRAPH_EVIDENCE_KIND, GRAPH_JOURNEY_STATUS, GRAPH_RELATION_KIND, GRAPH_TASK_MODE, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* The operational graph behind the Atlas view: a business spec of how
|
|
5
|
+
* a company operates, in its own language. Four fixed levels — line →
|
|
6
|
+
* journey → stage → task — plus named actors, RACI assignments,
|
|
7
|
+
* level-typed relations, execution bindings, and evidence. Automation
|
|
8
|
+
* status is always derived from bindings, never declared.
|
|
9
|
+
*/
|
|
10
|
+
const name = (description) => t.String({ minLength: 1, maxLength: 300, pattern: "\\S", description });
|
|
11
|
+
const longText = (description) => t.String({ maxLength: 5_000, description });
|
|
12
|
+
const position = t.Integer({
|
|
13
|
+
minimum: 0,
|
|
14
|
+
maximum: 100_000,
|
|
15
|
+
description: "Sort position within the parent (0-based)",
|
|
16
|
+
});
|
|
17
|
+
const urlField = t.String({
|
|
18
|
+
minLength: 1,
|
|
19
|
+
maxLength: 2_000,
|
|
20
|
+
description: "Link URL",
|
|
21
|
+
});
|
|
22
|
+
const referenceField = t.String({
|
|
23
|
+
minLength: 1,
|
|
24
|
+
maxLength: 1_000,
|
|
25
|
+
pattern: "\\S",
|
|
26
|
+
description: "External reference (system-specific identifier)",
|
|
27
|
+
});
|
|
28
|
+
export function registerGraphActions(rl) {
|
|
29
|
+
/* -------------------------------------------------------------- */
|
|
30
|
+
/* Reads */
|
|
31
|
+
/* -------------------------------------------------------------- */
|
|
32
|
+
rl.registerAction("summary", {
|
|
33
|
+
access: "read",
|
|
34
|
+
description: "The operational graph's structure without task bodies: every " +
|
|
35
|
+
"line, journey, and stage with rollups (total/liveAi/planned/" +
|
|
36
|
+
"human counts), plus actors and level labels. Small at any org " +
|
|
37
|
+
"size; the right default read.",
|
|
38
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
39
|
+
async execute(_input, ctx) {
|
|
40
|
+
const body = await request(ctx, `${ATLAS_BASE}/summary`);
|
|
41
|
+
return body.summary;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
rl.registerAction("graph.get", {
|
|
45
|
+
access: "read",
|
|
46
|
+
description: "The entire operational graph including every task body, " +
|
|
47
|
+
"assignment, binding, and evidence. Payload grows with task " +
|
|
48
|
+
"count — prefer summary plus stage.get.",
|
|
49
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
50
|
+
async execute(_input, ctx) {
|
|
51
|
+
const body = await request(ctx, `${ATLAS_BASE}/graph`);
|
|
52
|
+
return body.graph;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
rl.registerAction("stage.get", {
|
|
56
|
+
access: "read",
|
|
57
|
+
description: "One stage in full: its ordered tasks with participants, owner, " +
|
|
58
|
+
"bindings, evidence, and derived automation status.",
|
|
59
|
+
inputSchema: t.Object({ id: idSchema("Stage ID") }, STRICT_OBJECT),
|
|
60
|
+
async execute(input, ctx) {
|
|
61
|
+
const { id } = input;
|
|
62
|
+
const body = await request(ctx, `${ATLAS_BASE}/stages/${pathSegment(id)}`);
|
|
63
|
+
return body.stage;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
rl.registerAction("actor.list", {
|
|
67
|
+
access: "read",
|
|
68
|
+
description: "List the named workers of the graph: agents and human roles.",
|
|
69
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
70
|
+
async execute(_input, ctx) {
|
|
71
|
+
const body = await request(ctx, `${ATLAS_BASE}/actors`);
|
|
72
|
+
return body.actors;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
rl.registerAction("line.list", {
|
|
76
|
+
access: "read",
|
|
77
|
+
description: "List business lines. Every organization has one default line; " +
|
|
78
|
+
"journeys created without a lineId land there.",
|
|
79
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
80
|
+
async execute(_input, ctx) {
|
|
81
|
+
const body = await request(ctx, `${ATLAS_BASE}/lines`);
|
|
82
|
+
return body.lines;
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
rl.registerAction("journey.list", {
|
|
86
|
+
access: "read",
|
|
87
|
+
description: "List journeys (id, name, outcome, status, line).",
|
|
88
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
89
|
+
async execute(_input, ctx) {
|
|
90
|
+
const body = await request(ctx, `${ATLAS_BASE}/journeys`);
|
|
91
|
+
return body.journeys;
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
rl.registerAction("journey.get", {
|
|
95
|
+
access: "read",
|
|
96
|
+
description: "One journey in full: its stages, their tasks, triggers, and rollups.",
|
|
97
|
+
inputSchema: t.Object({ id: idSchema("Journey ID") }, STRICT_OBJECT),
|
|
98
|
+
async execute(input, ctx) {
|
|
99
|
+
const { id } = input;
|
|
100
|
+
const body = await request(ctx, `${ATLAS_BASE}/journeys/${pathSegment(id)}`);
|
|
101
|
+
return body.journey;
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
rl.registerAction("labels.get", {
|
|
105
|
+
access: "read",
|
|
106
|
+
description: "What this organization calls each level (line/journey/stage/task).",
|
|
107
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
108
|
+
async execute(_input, ctx) {
|
|
109
|
+
const body = await request(ctx, `${ATLAS_BASE}/labels`);
|
|
110
|
+
return body.labels;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
rl.registerAction("change.list", {
|
|
114
|
+
access: "read",
|
|
115
|
+
description: "Append-only change log of the graph, newest first, cursor-" +
|
|
116
|
+
"paginated. Returns { changes, nextCursor? }.",
|
|
117
|
+
inputSchema: t.Object({
|
|
118
|
+
entityType: t.Optional(enumSchema("Entity type", GRAPH_CHANGE_ENTITY_TYPE)),
|
|
119
|
+
entityId: t.Optional(idSchema("Entity ID")),
|
|
120
|
+
cursor: t.Optional(cursorSchema()),
|
|
121
|
+
limit: t.Optional(t.Integer({ minimum: 1, maximum: 200 })),
|
|
122
|
+
}, STRICT_OBJECT),
|
|
123
|
+
async execute(input, ctx) {
|
|
124
|
+
const params = new URLSearchParams();
|
|
125
|
+
for (const [key, value] of Object.entries(input)) {
|
|
126
|
+
if (value !== undefined)
|
|
127
|
+
params.set(key, String(value));
|
|
128
|
+
}
|
|
129
|
+
return request(ctx, `${ATLAS_BASE}/changes?${params}`);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
/* -------------------------------------------------------------- */
|
|
133
|
+
/* Mutations — each mirrors one service input schema. */
|
|
134
|
+
/* -------------------------------------------------------------- */
|
|
135
|
+
/**
|
|
136
|
+
* Sub-resource creates carry one routing key (journeyId, stageId,
|
|
137
|
+
* taskId) that belongs in the path, not the body. It is named as the
|
|
138
|
+
* key to omit — rather than allowlisting body fields — so a field
|
|
139
|
+
* added to the schema later flows through by default instead of
|
|
140
|
+
* being silently dropped.
|
|
141
|
+
*/
|
|
142
|
+
function create(action, entity, path, description, fields, routingKey) {
|
|
143
|
+
rl.registerAction(action, {
|
|
144
|
+
access: "write",
|
|
145
|
+
description,
|
|
146
|
+
inputSchema: t.Object(fields, STRICT_OBJECT),
|
|
147
|
+
async execute(input, ctx) {
|
|
148
|
+
const all = input;
|
|
149
|
+
const body = { ...all };
|
|
150
|
+
if (routingKey)
|
|
151
|
+
delete body[routingKey];
|
|
152
|
+
const response = await request(ctx, path(all), { method: "POST", body: JSON.stringify(body) });
|
|
153
|
+
return response[entity];
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function update(action, entity, collection, description, fields) {
|
|
158
|
+
rl.registerAction(action, {
|
|
159
|
+
access: "write",
|
|
160
|
+
description,
|
|
161
|
+
inputSchema: t.Object({ id: idSchema(`${entity} ID`), ...fields }, STRICT_UPDATE_OBJECT),
|
|
162
|
+
async execute(input, ctx) {
|
|
163
|
+
const { id, ...patch } = input;
|
|
164
|
+
const response = await request(ctx, `${ATLAS_BASE}/${collection}/${pathSegment(id)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
165
|
+
return response[entity];
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function remove(action, entity, collection, description) {
|
|
170
|
+
rl.registerAction(action, {
|
|
171
|
+
access: "write",
|
|
172
|
+
description,
|
|
173
|
+
inputSchema: t.Object({ id: idSchema(`${entity} ID`) }, STRICT_OBJECT),
|
|
174
|
+
async execute(input, ctx) {
|
|
175
|
+
const { id } = input;
|
|
176
|
+
await request(ctx, `${ATLAS_BASE}/${collection}/${pathSegment(id)}`, { method: "DELETE" });
|
|
177
|
+
return { deleted: true, id };
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/* Actors */
|
|
182
|
+
create("actor.create", "actor", () => `${ATLAS_BASE}/actors`, "Create a named worker: an agent (e.g. Jarvis) or a human role (e.g. CFO).", {
|
|
183
|
+
name: name("Actor name"),
|
|
184
|
+
kind: enumSchema("Actor kind", GRAPH_ACTOR_KIND),
|
|
185
|
+
description: t.Optional(longText("What this actor does")),
|
|
186
|
+
});
|
|
187
|
+
update("actor.update", "actor", "actors", "Update an actor.", {
|
|
188
|
+
name: t.Optional(name("Actor name")),
|
|
189
|
+
kind: t.Optional(enumSchema("Actor kind", GRAPH_ACTOR_KIND)),
|
|
190
|
+
description: t.Optional(longText("What this actor does")),
|
|
191
|
+
});
|
|
192
|
+
remove("actor.delete", "actor", "actors", "Delete an actor. Refused (409 actor_in_use) while assignments reference it.");
|
|
193
|
+
/* Lines */
|
|
194
|
+
create("line.create", "line", () => `${ATLAS_BASE}/lines`, "Create a business line — the widest grouping of journeys.", {
|
|
195
|
+
name: name("Line name"),
|
|
196
|
+
blurb: t.Optional(longText("One-sentence description")),
|
|
197
|
+
position: t.Optional(position),
|
|
198
|
+
});
|
|
199
|
+
update("line.update", "line", "lines", "Update a business line.", {
|
|
200
|
+
name: t.Optional(name("Line name")),
|
|
201
|
+
blurb: t.Optional(longText("One-sentence description")),
|
|
202
|
+
position: t.Optional(position),
|
|
203
|
+
});
|
|
204
|
+
remove("line.delete", "line", "lines", "Delete a business line. Refused while journeys reference it (line_in_use) or it is the last line (last_line).");
|
|
205
|
+
/* Journeys */
|
|
206
|
+
create("journey.create", "journey", () => `${ATLAS_BASE}/journeys`, "Create a journey: an end-to-end flow with a business outcome. " +
|
|
207
|
+
"Omitting lineId places it in the organization's default line.", {
|
|
208
|
+
lineId: t.Optional(idSchema("Business line ID")),
|
|
209
|
+
name: name("Journey name"),
|
|
210
|
+
outcome: t.Optional(longText("The business outcome it delivers")),
|
|
211
|
+
position: t.Optional(position),
|
|
212
|
+
status: t.Optional(enumSchema("Journey status", GRAPH_JOURNEY_STATUS)),
|
|
213
|
+
});
|
|
214
|
+
update("journey.update", "journey", "journeys", "Update a journey.", {
|
|
215
|
+
lineId: t.Optional(idSchema("Business line ID")),
|
|
216
|
+
name: t.Optional(name("Journey name")),
|
|
217
|
+
outcome: t.Optional(longText("The business outcome it delivers")),
|
|
218
|
+
position: t.Optional(position),
|
|
219
|
+
status: t.Optional(enumSchema("Journey status", GRAPH_JOURNEY_STATUS)),
|
|
220
|
+
});
|
|
221
|
+
remove("journey.delete", "journey", "journeys", "Delete a journey and, in cascade, its stages and tasks.");
|
|
222
|
+
/* Stages */
|
|
223
|
+
create("stage.create", "stage", (input) => `${ATLAS_BASE}/journeys/${pathSegment(input.journeyId)}/stages`, "Create an ordered stage inside a journey.", {
|
|
224
|
+
journeyId: idSchema("Journey ID"),
|
|
225
|
+
name: name("Stage name"),
|
|
226
|
+
intent: t.Optional(longText("What this stage exists to ensure")),
|
|
227
|
+
position: t.Optional(position),
|
|
228
|
+
}, "journeyId");
|
|
229
|
+
update("stage.update", "stage", "stages", "Update a stage.", {
|
|
230
|
+
name: t.Optional(name("Stage name")),
|
|
231
|
+
intent: t.Optional(longText("What this stage exists to ensure")),
|
|
232
|
+
position: t.Optional(position),
|
|
233
|
+
});
|
|
234
|
+
remove("stage.delete", "stage", "stages", "Delete a stage and, in cascade, its tasks.");
|
|
235
|
+
/* Tasks */
|
|
236
|
+
create("task.create", "task", (input) => `${ATLAS_BASE}/stages/${pathSegment(input.stageId)}/tasks`, "Create a task — the atomic responsibility inside a stage. " +
|
|
237
|
+
"actorId also creates the matching `does` assignment (the owner).", {
|
|
238
|
+
stageId: idSchema("Stage ID"),
|
|
239
|
+
description: t.String({
|
|
240
|
+
minLength: 1,
|
|
241
|
+
maxLength: 2_000,
|
|
242
|
+
pattern: "\\S",
|
|
243
|
+
description: "What the work is, in the business's own words",
|
|
244
|
+
}),
|
|
245
|
+
position: t.Optional(position),
|
|
246
|
+
intendedMode: enumSchema("Intended mode", GRAPH_TASK_MODE),
|
|
247
|
+
actorId: t.Optional(idSchema("Owner actor ID")),
|
|
248
|
+
}, "stageId");
|
|
249
|
+
update("task.update", "task", "tasks", "Update a task. actorId replaces the `does` assignment; null clears the owner.", {
|
|
250
|
+
description: t.Optional(t.String({ minLength: 1, maxLength: 2_000, pattern: "\\S" })),
|
|
251
|
+
position: t.Optional(position),
|
|
252
|
+
intendedMode: t.Optional(enumSchema("Intended mode", GRAPH_TASK_MODE)),
|
|
253
|
+
actorId: t.Optional(t.Union([idSchema("Owner actor ID"), t.Null()])),
|
|
254
|
+
});
|
|
255
|
+
remove("task.delete", "task", "tasks", "Delete a task.");
|
|
256
|
+
/* Assignments */
|
|
257
|
+
create("assignment.create", "assignment", (input) => `${ATLAS_BASE}/tasks/${pathSegment(input.taskId)}/assignments`, "Add an actor to a task in a RACI role. `does` is the owner; a task has at most one.", {
|
|
258
|
+
taskId: idSchema("Task ID"),
|
|
259
|
+
actorId: idSchema("Actor ID"),
|
|
260
|
+
role: t.Optional(enumSchema("Assignment role", GRAPH_ASSIGNMENT_ROLE)),
|
|
261
|
+
}, "taskId");
|
|
262
|
+
remove("assignment.delete", "assignment", "assignments", "Remove an actor's part in a task.");
|
|
263
|
+
/* Relations */
|
|
264
|
+
create("relation.create", "relation", () => `${ATLAS_BASE}/relations`, "Create a level-typed link: `follows` joins two tasks in one stage " +
|
|
265
|
+
"(cycles rejected); `triggers` joins two journeys (cycles allowed).", {
|
|
266
|
+
kind: enumSchema("Relation kind", GRAPH_RELATION_KIND),
|
|
267
|
+
fromId: idSchema("Source task or journey ID"),
|
|
268
|
+
toId: idSchema("Target task or journey ID"),
|
|
269
|
+
});
|
|
270
|
+
remove("relation.delete", "relation", "relations", "Delete a relation.");
|
|
271
|
+
/* Bindings */
|
|
272
|
+
create("binding.create", "binding", (input) => `${ATLAS_BASE}/tasks/${pathSegment(input.taskId)}/bindings`, "Bind a task to how it is executed: a Vex deployment, an external " +
|
|
273
|
+
"system, or manual work. A live non-manual binding is what makes " +
|
|
274
|
+
"a task count as live AI.", {
|
|
275
|
+
taskId: idSchema("Task ID"),
|
|
276
|
+
executor: enumSchema("Executor", GRAPH_BINDING_EXECUTOR),
|
|
277
|
+
label: name("Human-readable binding label"),
|
|
278
|
+
status: t.Optional(enumSchema("Binding status", GRAPH_BINDING_STATUS)),
|
|
279
|
+
deploymentId: t.Optional(idSchema("Vex deployment ID")),
|
|
280
|
+
reference: t.Optional(referenceField),
|
|
281
|
+
url: t.Optional(urlField),
|
|
282
|
+
}, "taskId");
|
|
283
|
+
update("binding.update", "binding", "bindings", "Update a binding; null clears deploymentId, reference, or url.", {
|
|
284
|
+
executor: t.Optional(enumSchema("Executor", GRAPH_BINDING_EXECUTOR)),
|
|
285
|
+
label: t.Optional(name("Human-readable binding label")),
|
|
286
|
+
status: t.Optional(enumSchema("Binding status", GRAPH_BINDING_STATUS)),
|
|
287
|
+
deploymentId: t.Optional(t.Union([idSchema("Vex deployment ID"), t.Null()])),
|
|
288
|
+
reference: t.Optional(t.Union([referenceField, t.Null()])),
|
|
289
|
+
url: t.Optional(t.Union([urlField, t.Null()])),
|
|
290
|
+
});
|
|
291
|
+
remove("binding.delete", "binding", "bindings", "Delete a binding.");
|
|
292
|
+
/* Evidence */
|
|
293
|
+
create("evidence.create", "evidence", (input) => `${ATLAS_BASE}/tasks/${pathSegment(input.taskId)}/evidence`, "Attach proof the work happens: a report, artifact, or dashboard. " +
|
|
294
|
+
"Requires url or artifactRef.", {
|
|
295
|
+
taskId: idSchema("Task ID"),
|
|
296
|
+
title: name("Evidence title"),
|
|
297
|
+
kind: enumSchema("Evidence kind", GRAPH_EVIDENCE_KIND),
|
|
298
|
+
url: t.Optional(urlField),
|
|
299
|
+
artifactRef: t.Optional(referenceField),
|
|
300
|
+
}, "taskId");
|
|
301
|
+
update("evidence.update", "evidence", "evidence", "Update evidence; null clears url or artifactRef.", {
|
|
302
|
+
title: t.Optional(name("Evidence title")),
|
|
303
|
+
kind: t.Optional(enumSchema("Evidence kind", GRAPH_EVIDENCE_KIND)),
|
|
304
|
+
url: t.Optional(t.Union([urlField, t.Null()])),
|
|
305
|
+
artifactRef: t.Optional(t.Union([referenceField, t.Null()])),
|
|
306
|
+
});
|
|
307
|
+
remove("evidence.delete", "evidence", "evidence", "Delete an evidence entry.");
|
|
308
|
+
/* Labels */
|
|
309
|
+
rl.registerAction("labels.update", {
|
|
310
|
+
access: "write",
|
|
311
|
+
description: "Rename what this organization calls each level — display only, " +
|
|
312
|
+
"no structural consequence.",
|
|
313
|
+
inputSchema: t.Object({
|
|
314
|
+
line: t.Optional(name("Label for the line level")),
|
|
315
|
+
journey: t.Optional(name("Label for the journey level")),
|
|
316
|
+
stage: t.Optional(name("Label for the stage level")),
|
|
317
|
+
task: t.Optional(name("Label for the task level")),
|
|
318
|
+
}, { additionalProperties: false, minProperties: 1 }),
|
|
319
|
+
async execute(input, ctx) {
|
|
320
|
+
const body = await request(ctx, `${ATLAS_BASE}/labels`, { method: "PATCH", body: JSON.stringify(input) });
|
|
321
|
+
return body.labels;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { registerGraphActions } from "./graph.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shift Atlas — the operational graph of a Shift cloud organization:
|
|
5
|
+
* the map of how the business runs. Independent of the shiftLabs
|
|
6
|
+
* plugin the same way shiftCrm is, but authenticated with the same
|
|
7
|
+
* Shift cloud API key; the cloud derives the organization from the
|
|
8
|
+
* key, so no organization ID is ever sent (SHFT-852).
|
|
9
|
+
*/
|
|
10
|
+
export default function shiftAtlas(rl) {
|
|
11
|
+
rl.setName("shiftAtlas");
|
|
12
|
+
rl.setVersion("0.1.0");
|
|
13
|
+
rl.setConnectionSchema(t.Object({
|
|
14
|
+
// Same Shift Labs API key family as the shiftLabs plugin; needs
|
|
15
|
+
// the service:operational-graph:use scope.
|
|
16
|
+
apiKey: t.String({
|
|
17
|
+
description: "Shift Labs API key",
|
|
18
|
+
env: "SHIFT_LABS_API_KEY",
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
registerGraphActions(rl);
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { cursorSchema, enumSchema, idSchema, pathSegment, request, STRICT_OBJECT, STRICT_UPDATE_OBJECT, } from "../../_shared/shiftCloud.js";
|
|
2
|
+
/** Base path of the operational graph service in the Shift cloud API. */
|
|
3
|
+
export const ATLAS_BASE = "/v1/services/operational-graph";
|
|
4
|
+
export const GRAPH_ACTOR_KIND = ["agent", "human_role"];
|
|
5
|
+
export const GRAPH_JOURNEY_STATUS = ["draft", "published", "retired"];
|
|
6
|
+
export const GRAPH_ASSIGNMENT_ROLE = [
|
|
7
|
+
"does",
|
|
8
|
+
"approves",
|
|
9
|
+
"consulted",
|
|
10
|
+
"informed",
|
|
11
|
+
];
|
|
12
|
+
export const GRAPH_RELATION_KIND = ["follows", "triggers"];
|
|
13
|
+
export const GRAPH_TASK_MODE = ["ai", "human", "hybrid"];
|
|
14
|
+
export const GRAPH_BINDING_EXECUTOR = ["vex", "external", "manual"];
|
|
15
|
+
export const GRAPH_BINDING_STATUS = ["planned", "live", "paused"];
|
|
16
|
+
export const GRAPH_EVIDENCE_KIND = ["report", "artifact", "dashboard"];
|
|
17
|
+
export const GRAPH_CHANGE_ENTITY_TYPE = [
|
|
18
|
+
"actor",
|
|
19
|
+
"line",
|
|
20
|
+
"journey",
|
|
21
|
+
"stage",
|
|
22
|
+
"task",
|
|
23
|
+
"assignment",
|
|
24
|
+
"relation",
|
|
25
|
+
"binding",
|
|
26
|
+
"evidence",
|
|
27
|
+
"labels",
|
|
28
|
+
];
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import * as t from "typebox";
|
|
4
|
+
import { request, STRICT_OBJECT } from "../../_shared/shiftCloud.js";
|
|
5
|
+
/**
|
|
6
|
+
* Shift OCR — text and structured-field extraction from images and
|
|
7
|
+
* PDFs through the Shift cloud OCR service. The API key is the tenant
|
|
8
|
+
* authority; no organization ID is ever sent (SHFT-852).
|
|
9
|
+
*/
|
|
10
|
+
const IMAGE_TYPES = {
|
|
11
|
+
avif: "image/avif",
|
|
12
|
+
gif: "image/gif",
|
|
13
|
+
jpeg: "image/jpeg",
|
|
14
|
+
jpg: "image/jpeg",
|
|
15
|
+
png: "image/png",
|
|
16
|
+
webp: "image/webp",
|
|
17
|
+
};
|
|
18
|
+
const DOCUMENT_TYPES = {
|
|
19
|
+
pdf: "application/pdf",
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Raw-file ceiling for inlining as a base64 data URL. Base64 inflates
|
|
23
|
+
* by 4/3, so 20 MiB raw stays well inside provider request limits.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_FILE_BYTES = 20 * 1024 * 1024;
|
|
26
|
+
async function documentFromPath(path) {
|
|
27
|
+
const extension = extname(path).slice(1).toLowerCase();
|
|
28
|
+
const imageType = IMAGE_TYPES[extension];
|
|
29
|
+
const documentType = DOCUMENT_TYPES[extension];
|
|
30
|
+
if (!imageType && !documentType) {
|
|
31
|
+
throw new Error(`Unsupported file extension for ${path}. Supported: ${[
|
|
32
|
+
...Object.keys(IMAGE_TYPES),
|
|
33
|
+
...Object.keys(DOCUMENT_TYPES),
|
|
34
|
+
].join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
const file = await stat(path);
|
|
37
|
+
if (!file.isFile())
|
|
38
|
+
throw new Error(`${path} is not a file`);
|
|
39
|
+
if (file.size === 0)
|
|
40
|
+
throw new Error("File is empty");
|
|
41
|
+
if (file.size > MAX_FILE_BYTES) {
|
|
42
|
+
throw new Error(`File is ${file.size} bytes; inline OCR uploads are capped at ${MAX_FILE_BYTES} bytes. Host the file and pass url instead.`);
|
|
43
|
+
}
|
|
44
|
+
const mediaType = imageType ?? documentType;
|
|
45
|
+
const url = `data:${mediaType};base64,${(await readFile(path)).toString("base64")}`;
|
|
46
|
+
return imageType
|
|
47
|
+
? { type: "image", url }
|
|
48
|
+
: { type: "document", url, name: path.replace(/.*\//, "") };
|
|
49
|
+
}
|
|
50
|
+
function documentFromUrl(url, kind) {
|
|
51
|
+
if (kind)
|
|
52
|
+
return { type: kind, url };
|
|
53
|
+
// Data URLs carry their media type, not an extension.
|
|
54
|
+
if (url.startsWith("data:")) {
|
|
55
|
+
return url.startsWith("data:application/pdf")
|
|
56
|
+
? { type: "document", url }
|
|
57
|
+
: { type: "image", url };
|
|
58
|
+
}
|
|
59
|
+
const extension = new URL(url).pathname.replace(/.*\./, "").toLowerCase();
|
|
60
|
+
return DOCUMENT_TYPES[extension]
|
|
61
|
+
? { type: "document", url }
|
|
62
|
+
: { type: "image", url };
|
|
63
|
+
}
|
|
64
|
+
export default function shiftOcr(rl) {
|
|
65
|
+
rl.setName("shiftOcr");
|
|
66
|
+
rl.setVersion("0.1.0");
|
|
67
|
+
rl.setConnectionSchema(t.Object({
|
|
68
|
+
// Same Shift Labs API key as the shiftLabs plugin; the cloud
|
|
69
|
+
// derives the organization from it, so no org ID is needed.
|
|
70
|
+
apiKey: t.String({
|
|
71
|
+
description: "Shift Labs API key",
|
|
72
|
+
env: "SHIFT_LABS_API_KEY",
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
rl.registerAction("ocr.extract", {
|
|
76
|
+
access: "write",
|
|
77
|
+
description: "Extract markdown text — and optionally schema-constrained JSON — " +
|
|
78
|
+
"from an image or PDF. Pass a local file path (inlined as a data " +
|
|
79
|
+
"URL) or an https URL. Provide schema to get structured fields " +
|
|
80
|
+
"back instead of parsing the text yourself.",
|
|
81
|
+
inputSchema: t.Object({
|
|
82
|
+
path: t.Optional(t.String({
|
|
83
|
+
minLength: 1,
|
|
84
|
+
pattern: "\\S",
|
|
85
|
+
description: "Host path of an image or PDF (max 20 MiB)",
|
|
86
|
+
})),
|
|
87
|
+
url: t.Optional(t.String({
|
|
88
|
+
minLength: 1,
|
|
89
|
+
pattern: "\\S",
|
|
90
|
+
description: "https or data URL of the image or PDF",
|
|
91
|
+
})),
|
|
92
|
+
kind: t.Optional(t.Union([t.Literal("image"), t.Literal("document")], {
|
|
93
|
+
description: "Override for URLs whose extension does not reveal the type. " +
|
|
94
|
+
"Inferred from the extension otherwise.",
|
|
95
|
+
})),
|
|
96
|
+
pages: t.Optional(t.String({
|
|
97
|
+
minLength: 1,
|
|
98
|
+
pattern: "^\\d+(-\\d+)?(,\\d+(-\\d+)?)*$",
|
|
99
|
+
description: "Zero-based pages for PDFs, e.g. '0', '0-5', or '0,2-4'",
|
|
100
|
+
})),
|
|
101
|
+
schema: t.Optional(t.Record(t.String(), t.Unknown(), {
|
|
102
|
+
description: "JSON schema for structured extraction across the document",
|
|
103
|
+
})),
|
|
104
|
+
schemaName: t.Optional(t.String({
|
|
105
|
+
minLength: 1,
|
|
106
|
+
pattern: "\\S",
|
|
107
|
+
description: "Extractor name for provenance. Default: extraction",
|
|
108
|
+
})),
|
|
109
|
+
prompt: t.Optional(t.String({
|
|
110
|
+
minLength: 1,
|
|
111
|
+
pattern: "\\S",
|
|
112
|
+
description: "Guidance for structured extraction; requires schema",
|
|
113
|
+
})),
|
|
114
|
+
}, STRICT_OBJECT),
|
|
115
|
+
async execute(input, ctx) {
|
|
116
|
+
const fields = input;
|
|
117
|
+
if (!fields.path === !fields.url) {
|
|
118
|
+
throw new Error("Provide exactly one of path or url");
|
|
119
|
+
}
|
|
120
|
+
if (fields.prompt && !fields.schema) {
|
|
121
|
+
throw new Error("prompt requires schema");
|
|
122
|
+
}
|
|
123
|
+
const document = fields.path
|
|
124
|
+
? await documentFromPath(fields.path)
|
|
125
|
+
: documentFromUrl(fields.url, fields.kind);
|
|
126
|
+
return await request(ctx, "/v1/services/ocr/extract", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
document,
|
|
130
|
+
...(fields.pages ? { pages: fields.pages } : {}),
|
|
131
|
+
...(fields.schema
|
|
132
|
+
? {
|
|
133
|
+
structured: {
|
|
134
|
+
name: fields.schemaName ?? "extraction",
|
|
135
|
+
schema: fields.schema,
|
|
136
|
+
...(fields.prompt ? { prompt: fields.prompt } : {}),
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
rl.registerAction("ocr.providers", {
|
|
145
|
+
access: "read",
|
|
146
|
+
description: "List the OCR providers and models available.",
|
|
147
|
+
inputSchema: t.Object({}, STRICT_OBJECT),
|
|
148
|
+
async execute(_input, ctx) {
|
|
149
|
+
const body = await request(ctx, "/v1/services/ocr/providers");
|
|
150
|
+
return body.providers;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
package/dist/sdk.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExecuteResult } from "./core/engine.js";
|
|
1
|
+
import { type ActionInvocation, type ExecuteResult } from "./core/engine.js";
|
|
2
2
|
import type { PluginFunction } from "./plugin/api.js";
|
|
3
3
|
import type { ActionAccess, ConnectionConfig, InputSchema, PluginDef } from "./plugin/types.js";
|
|
4
4
|
export interface RunlineOptions {
|
|
@@ -11,6 +11,15 @@ export interface RunlineOptions {
|
|
|
11
11
|
* `ExecutionEngine` — this bounds cross-run contamination, not memory.
|
|
12
12
|
*/
|
|
13
13
|
maxRunsPerWorker?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Observer fired in the host process for every action invocation
|
|
16
|
+
* that enters a plugin's `execute()` — unknown paths and failed
|
|
17
|
+
* input validation never reach plugin code and are not reported.
|
|
18
|
+
* Exceptions thrown by the observer are swallowed: observability
|
|
19
|
+
* never breaks a run. Not part of `RunlineConfig` on purpose —
|
|
20
|
+
* config round-trips through JSON (`fromProject`), functions do not.
|
|
21
|
+
*/
|
|
22
|
+
onAction?: (info: ActionInvocation) => void;
|
|
14
23
|
}
|
|
15
24
|
export interface RunlineExecuteOptions {
|
|
16
25
|
timeoutMs?: number;
|
|
@@ -25,6 +34,7 @@ export declare class Runline {
|
|
|
25
34
|
* leak) a fresh worker every time — the exact bug pooling exists to fix.
|
|
26
35
|
*/
|
|
27
36
|
private _engine;
|
|
37
|
+
private readonly _onAction;
|
|
28
38
|
private constructor();
|
|
29
39
|
private engine;
|
|
30
40
|
/**
|
package/dist/sdk.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { DEFAULT_CONFIG } from "./config/types.js";
|
|
4
|
-
import { ExecutionEngine } from "./core/engine.js";
|
|
4
|
+
import { ExecutionEngine, } from "./core/engine.js";
|
|
5
5
|
import { resolvePluginExport } from "./plugin/api.js";
|
|
6
6
|
import { discoverPlugins } from "./plugin/loader.js";
|
|
7
7
|
import { PluginRegistry } from "./plugin/registry.js";
|
|
@@ -14,8 +14,10 @@ export class Runline {
|
|
|
14
14
|
* leak) a fresh worker every time — the exact bug pooling exists to fix.
|
|
15
15
|
*/
|
|
16
16
|
_engine = null;
|
|
17
|
+
_onAction;
|
|
17
18
|
constructor(options) {
|
|
18
19
|
this._registry = new PluginRegistry();
|
|
20
|
+
this._onAction = options.onAction;
|
|
19
21
|
for (const pluginOrFn of options.plugins ?? []) {
|
|
20
22
|
const plugin = resolvePluginExport(pluginOrFn, "unknown");
|
|
21
23
|
this._registry.register(plugin);
|
|
@@ -31,7 +33,9 @@ export class Runline {
|
|
|
31
33
|
}
|
|
32
34
|
engine() {
|
|
33
35
|
if (!this._engine) {
|
|
34
|
-
this._engine = new ExecutionEngine(this._registry, this._config
|
|
36
|
+
this._engine = new ExecutionEngine(this._registry, this._config, {
|
|
37
|
+
onAction: this._onAction,
|
|
38
|
+
});
|
|
35
39
|
}
|
|
36
40
|
return this._engine;
|
|
37
41
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Code mode for agents
|
|
3
|
+
"version": "0.21.0",
|
|
4
|
+
"description": "Code mode for agents \u2014 turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|