runline 0.19.0 → 0.20.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 +27 -1
- package/dist/core/engine.js +32 -2
- package/dist/index.d.ts +1 -1
- 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,30 @@ 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 the thrown value, unmodified, when execute
|
|
18
|
+
* rejected; the hook sees it in the host process even though the
|
|
19
|
+
* worker only ever receives the message string.
|
|
20
|
+
*/
|
|
21
|
+
export interface ActionInvocation {
|
|
22
|
+
plugin: string;
|
|
23
|
+
action: string;
|
|
24
|
+
/** The full callable path, "plugin.action". */
|
|
25
|
+
path: string;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
error?: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface EngineHooks {
|
|
30
|
+
/**
|
|
31
|
+
* Observer for every action invocation. Must not throw — but if it
|
|
32
|
+
* does, the engine swallows it: observability never breaks a run.
|
|
33
|
+
*/
|
|
34
|
+
onAction?: (info: ActionInvocation) => void;
|
|
35
|
+
}
|
|
12
36
|
/**
|
|
13
37
|
* Whether to arm the host-side RSS watchdog for a worker run.
|
|
14
38
|
*
|
|
@@ -48,9 +72,10 @@ export declare function shouldArmRssWatchdog(versions?: Partial<Record<string, s
|
|
|
48
72
|
export declare class ExecutionEngine {
|
|
49
73
|
private registry;
|
|
50
74
|
private config;
|
|
75
|
+
private hooks;
|
|
51
76
|
private pooled;
|
|
52
77
|
private readonly maxRunsPerWorker;
|
|
53
|
-
constructor(registry: PluginRegistry, config: RunlineConfig);
|
|
78
|
+
constructor(registry: PluginRegistry, config: RunlineConfig, hooks?: EngineHooks);
|
|
54
79
|
/** Retire the pooled worker, if any. Safe to call repeatedly. */
|
|
55
80
|
dispose(): void;
|
|
56
81
|
/**
|
|
@@ -71,6 +96,7 @@ export declare class ExecutionEngine {
|
|
|
71
96
|
private release;
|
|
72
97
|
execute(code: string, options?: EngineOptions): Promise<ExecuteResult>;
|
|
73
98
|
private invokeAction;
|
|
99
|
+
private fireOnAction;
|
|
74
100
|
/**
|
|
75
101
|
* A miss on "salesforce.status" is often a plugin that registered
|
|
76
102
|
* 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,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.20.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": {
|