@jini-ai/server 0.5.1 → 0.6.1
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/builtin-features.js +3 -0
- package/dist/storage/legacy/sqlite.d.ts +16 -11
- package/dist/storage/legacy/sqlite.js +47 -38
- package/package.json +12 -12
- package/dist/frontend-control.d.ts +0 -76
- package/dist/frontend-control.js +0 -77
package/dist/builtin-features.js
CHANGED
|
@@ -232,6 +232,9 @@ export function createBuiltInFeatures(options = {}) {
|
|
|
232
232
|
registerRunRoutes({ app: app, deps: {
|
|
233
233
|
lifecycle: services.context.kernel.lifecycle,
|
|
234
234
|
...(onStarted === undefined ? {} : { onStarted }),
|
|
235
|
+
// A mid-run message reaches whichever live run the kernel's own executor is driving;
|
|
236
|
+
// a run some other driver owns reads as not-running, never as delivered.
|
|
237
|
+
deliverUserMessage: (input) => services.context.kernel.agentExecutor.sendUserMessage(input),
|
|
235
238
|
}, adapter: services.context.adapter });
|
|
236
239
|
},
|
|
237
240
|
}),
|
|
@@ -8,19 +8,24 @@ import { type SqliteDb, type SqliteSyncOpener } from '@jini-ai/db/sqlite';
|
|
|
8
8
|
export declare function migrate({ db }: {
|
|
9
9
|
db: SqliteDb;
|
|
10
10
|
}): void;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
/** Creates one owned connection cache; other hosts' paths and handles are independent.
|
|
12
|
+
* Returns lazy open/close ports; creation performs no I/O. @complexity O(1). */
|
|
13
|
+
export declare function createLegacySqliteStorage(_required: Record<string, never>, _optional?: Record<string, never>): {
|
|
14
|
+
openDatabase: ({ projectRoot, open }: {
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
open: SqliteSyncOpener;
|
|
17
|
+
}, options?: {
|
|
18
|
+
dataDir?: string;
|
|
19
|
+
filesystem?: Pick<typeof fs, "mkdirSync">;
|
|
20
|
+
}) => SqliteDb;
|
|
21
|
+
closeDatabase: () => void;
|
|
22
|
+
};
|
|
23
|
+
export declare const openDatabase: ({ projectRoot, open }: {
|
|
18
24
|
projectRoot: string;
|
|
19
25
|
open: SqliteSyncOpener;
|
|
20
26
|
}, options?: {
|
|
21
27
|
dataDir?: string;
|
|
22
|
-
filesystem?: Pick<typeof fs,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
export declare function closeDatabase(): void;
|
|
28
|
+
filesystem?: Pick<typeof fs, "mkdirSync">;
|
|
29
|
+
}) => SqliteDb;
|
|
30
|
+
export declare const closeDatabase: () => void;
|
|
26
31
|
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Host-owned app.sqlite lifecycle; the host explicitly supplies its SQLite driver. */
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { openSqliteConnection } from '@jini-ai/db/sqlite';
|
|
5
5
|
import { LEGACY_CHAT_DDL } from '@jini-ai/chat/store/legacy/sqlite';
|
|
6
6
|
import { LEGACY_AGENT_SESSIONS_DDL } from '@jini-ai/daemon/store/agent-sessions/sqlite';
|
|
7
7
|
import { LEGACY_PROJECTS_DDL } from '../../store/projects/sqlite.js';
|
|
8
|
-
let dbInstance = null;
|
|
9
|
-
let dbFile = null;
|
|
10
8
|
/**
|
|
11
9
|
* Explicit idempotent bootstrap of unchanged legacy tables; no versioned migration ledger.
|
|
12
10
|
* @param db Borrowed host handle; this operation neither opens nor closes it.
|
|
@@ -21,43 +19,54 @@ let dbFile = null;
|
|
|
21
19
|
export function migrate({ db }) {
|
|
22
20
|
db.exec(LEGACY_PROJECTS_DDL + LEGACY_CHAT_DDL + LEGACY_AGENT_SESSIONS_DDL);
|
|
23
21
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return db;
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
22
|
+
/** Creates one owned connection cache; other hosts' paths and handles are independent.
|
|
23
|
+
* Returns lazy open/close ports; creation performs no I/O. @complexity O(1). */
|
|
24
|
+
export function createLegacySqliteStorage(_required, _optional = {}) {
|
|
25
|
+
let dbInstance = null;
|
|
26
|
+
let dbFile = null;
|
|
27
|
+
/**
|
|
28
|
+
* Opens/caches <dataDir>/app.sqlite using the injected opener, WAL and foreign keys.
|
|
29
|
+
* A new path closes the previous handle owned by this storage instance; setup failure closes only the newly acquired handle.
|
|
30
|
+
* @throws The original host acquisition/schema failure, or a missing opener.
|
|
31
|
+
* @complexity O(1) acquisition/bootstrap calls, independent of stored row count.
|
|
32
|
+
*/
|
|
33
|
+
function openDatabase({ projectRoot, open }, options = {}) {
|
|
34
|
+
// Keep the historical refusal text for JavaScript callers that omit the required port.
|
|
35
|
+
if (!open)
|
|
36
|
+
throw new Error('openDatabase: inject options.open');
|
|
37
|
+
const dir = options.dataDir ? path.resolve(options.dataDir) : path.join(projectRoot, '.jini');
|
|
38
|
+
const file = path.join(dir, 'app.sqlite');
|
|
39
|
+
if (dbInstance && dbFile === file)
|
|
40
|
+
return dbInstance;
|
|
41
|
+
closeDatabase();
|
|
42
|
+
(options.filesystem ?? fs).mkdirSync(dir, { recursive: true });
|
|
43
|
+
let db;
|
|
49
44
|
try {
|
|
50
|
-
db
|
|
45
|
+
db = openSqliteConnection({ filePath: file, open({ filePath }, settings) { db = open(filePath, settings); return db; } }, { pragmas: ['journal_mode = WAL', 'foreign_keys = ON'] });
|
|
46
|
+
migrate({ db });
|
|
47
|
+
dbInstance = db;
|
|
48
|
+
dbFile = file;
|
|
49
|
+
return db;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
try {
|
|
53
|
+
db?.close();
|
|
54
|
+
}
|
|
55
|
+
catch { /* Keep the original bootstrap failure. */ }
|
|
56
|
+
throw error;
|
|
51
57
|
}
|
|
52
|
-
catch { /* Keep the original bootstrap failure. */ }
|
|
53
|
-
throw error;
|
|
54
58
|
}
|
|
59
|
+
/** Close the active owned handle once; clearing state first permits a clean retry after failure. */
|
|
60
|
+
function closeDatabase() {
|
|
61
|
+
const db = dbInstance;
|
|
62
|
+
dbInstance = null;
|
|
63
|
+
dbFile = null;
|
|
64
|
+
db?.close();
|
|
65
|
+
}
|
|
66
|
+
return { openDatabase, closeDatabase };
|
|
55
67
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
dbFile = null;
|
|
61
|
-
db?.close();
|
|
62
|
-
}
|
|
68
|
+
// Compatibility facade for existing hosts; new hosts inject their own storage instance.
|
|
69
|
+
const defaultStorage = createLegacySqliteStorage({});
|
|
70
|
+
export const openDatabase = defaultStorage.openDatabase;
|
|
71
|
+
export const closeDatabase = defaultStorage.closeDatabase;
|
|
63
72
|
//# sourceMappingURL=sqlite.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,27 +65,27 @@
|
|
|
65
65
|
"@vitest/coverage-v8": "^2.1.9",
|
|
66
66
|
"better-sqlite3": "^13.0.0",
|
|
67
67
|
"express": "^4.21.0",
|
|
68
|
-
"@jini-ai/
|
|
69
|
-
"@jini-ai/http-kit": "0.4.1",
|
|
70
|
-
"@jini-ai/daemon": "0.5.4",
|
|
71
|
-
"@jini-ai/agent-runtime": "0.4.2",
|
|
68
|
+
"@jini-ai/agent-runtime": "0.4.5",
|
|
72
69
|
"@jini-ai/sidecar": "0.4.1",
|
|
73
|
-
"@jini-ai/
|
|
74
|
-
"@jini-ai/
|
|
70
|
+
"@jini-ai/daemon": "0.6.1",
|
|
71
|
+
"@jini-ai/db": "0.3.2",
|
|
72
|
+
"@jini-ai/http-kit": "0.4.1",
|
|
73
|
+
"@jini-ai/core": "0.4.3",
|
|
74
|
+
"@jini-ai/chat": "0.5.8",
|
|
75
75
|
"@jini-ai/registry": "0.5.2"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@jini-ai/core": "^0.4.
|
|
78
|
+
"@jini-ai/core": "^0.4.3"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"better-sqlite3": "^13.0.0",
|
|
82
|
-
"@jini-ai/agent-runtime": "^0.4.
|
|
83
|
-
"@jini-ai/daemon": "^0.
|
|
82
|
+
"@jini-ai/agent-runtime": "^0.4.5",
|
|
83
|
+
"@jini-ai/daemon": "^0.6.1",
|
|
84
84
|
"@jini-ai/http-kit": "^0.4.0",
|
|
85
85
|
"@jini-ai/sidecar": "^0.4.0",
|
|
86
86
|
"express": "^4.21.0",
|
|
87
|
-
"@jini-ai/db": "^0.3.
|
|
88
|
-
"@jini-ai/chat": "^0.5.
|
|
87
|
+
"@jini-ai/db": "^0.3.2",
|
|
88
|
+
"@jini-ai/chat": "^0.5.8",
|
|
89
89
|
"@jini-ai/registry": "^0.5.1"
|
|
90
90
|
},
|
|
91
91
|
"peerDependenciesMeta": {
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { ToolPolicy, ToolRegistration } from '@jini-ai/core';
|
|
2
|
-
import { type FrontendCapabilitySpec } from '@jini-ai/daemon';
|
|
3
|
-
import { type RunCreateRequest, type RunStartHandler } from '@jini-ai/http-kit';
|
|
4
|
-
import type { LocalNodeHttpExtension } from './create-local-node-daemon.js';
|
|
5
|
-
export interface FrontendBindErrorContext {
|
|
6
|
-
readonly runId: string;
|
|
7
|
-
readonly error: unknown;
|
|
8
|
-
}
|
|
9
|
-
export interface CreateFrontendControlOptions {
|
|
10
|
-
/**
|
|
11
|
-
* The capabilities to expose. `@jini-ai/agentic`'s `PAGE_CAPABILITIES` and `@jini-ai/chat-core`'s
|
|
12
|
-
* `CHAT_CAPABILITIES` satisfy this structurally — the engine never imports that vocabulary
|
|
13
|
-
* (see `frontend-capability-tools.ts`'s module doc for why the edge points one way).
|
|
14
|
-
*/
|
|
15
|
-
readonly capabilities: readonly FrontendCapabilitySpec[];
|
|
16
|
-
/**
|
|
17
|
-
* Reads this run's bind token out of the host's own run request, or returns `undefined` when the
|
|
18
|
-
* run has no originating surface.
|
|
19
|
-
*
|
|
20
|
-
* Required, and deliberately not defaulted. A default would have to guess where a host keeps the
|
|
21
|
-
* token, and guessing wrong fails *open* in the confusing direction: the run starts, the agent
|
|
22
|
-
* gets its tools, and every page call fails later with "no frontend is bound" — a symptom that
|
|
23
|
-
* looks like a browser problem and is actually a config one.
|
|
24
|
-
*
|
|
25
|
-
* Returning `undefined` is normal, not an error. A run started from a CLI has no surface, and
|
|
26
|
-
* capabilities whose `surface` is `'server'` do not need one.
|
|
27
|
-
*/
|
|
28
|
-
readonly resolveBindToken: (request: RunCreateRequest) => string | undefined;
|
|
29
|
-
/** @default `@jini-ai/daemon`'s `denyAllFrontendCapabilityPolicy` — a host grants access explicitly. */
|
|
30
|
-
readonly policy?: ToolPolicy;
|
|
31
|
-
/** @default `@jini-ai/daemon`'s `DEFAULT_FRONTEND_CAPABILITY_TIMEOUT_MS`. */
|
|
32
|
-
readonly timeoutMs?: number;
|
|
33
|
-
/** Applied to every registration. Frontend output is untrusted page text; bound it. */
|
|
34
|
-
readonly maxOutputBytes?: number;
|
|
35
|
-
/**
|
|
36
|
-
* Host-owned sink for a bind failure. Defaults to `console.error`.
|
|
37
|
-
*
|
|
38
|
-
* A failed bind does not fail the run. The run is still legitimate — the agent simply cannot
|
|
39
|
-
* drive the page, and every capability call it makes will say so by name. Killing a run because
|
|
40
|
-
* one optional channel could not be established would turn a degraded session into no session.
|
|
41
|
-
*/
|
|
42
|
-
readonly onBindError?: (context: FrontendBindErrorContext) => void;
|
|
43
|
-
}
|
|
44
|
-
export interface FrontendControl {
|
|
45
|
-
/** Mounts the frontend-session stream and response routes. Pass in `httpExtensions`. */
|
|
46
|
-
readonly httpExtension: LocalNodeHttpExtension;
|
|
47
|
-
/** One gated tool per capability. Pass in `toolRegistrations`. */
|
|
48
|
-
readonly toolRegistrations: readonly ToolRegistration[];
|
|
49
|
-
/**
|
|
50
|
-
* Binds each starting run to the surface that originated it, and releases the binding when the
|
|
51
|
-
* run reaches a terminal state so a long-lived tab does not accumulate dead bindings.
|
|
52
|
-
*
|
|
53
|
-
* Compose it with whatever the host already does on run start — this hook only binds, it never
|
|
54
|
-
* executes anything:
|
|
55
|
-
*
|
|
56
|
-
* ```ts
|
|
57
|
-
* onRunStarted: (context) => {
|
|
58
|
-
* frontend.bindOnStarted(context);
|
|
59
|
-
* void startMyAgent(context);
|
|
60
|
-
* }
|
|
61
|
-
* ```
|
|
62
|
-
*/
|
|
63
|
-
readonly bindOnStarted: RunStartHandler;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Builds the frontend-control bundle.
|
|
67
|
-
*
|
|
68
|
-
* @param options - Capabilities to expose, how to find the bind token, and the gate to apply.
|
|
69
|
-
* @returns An HTTP extension, tool registrations, and a run-start bind hook — never the registry.
|
|
70
|
-
* @throws Never at construction. A duplicate capability id throws later, when the registrations are
|
|
71
|
-
* registered, naming the id (`ToolRegistry.register`'s own error).
|
|
72
|
-
*
|
|
73
|
-
* @complexity O(n) in `capabilities`.
|
|
74
|
-
*/
|
|
75
|
-
export declare function createFrontendControl(options: CreateFrontendControlOptions): FrontendControl;
|
|
76
|
-
//# sourceMappingURL=frontend-control.d.ts.map
|
package/dist/frontend-control.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { createFrontendCapabilityRegistrations, createFrontendSessionRegistry, } from '@jini-ai/daemon';
|
|
2
|
-
import { registerFrontendSessionRoutes, } from '@jini-ai/http-kit';
|
|
3
|
-
function defaultBindErrorSink(context) {
|
|
4
|
-
// eslint-disable-next-line no-console
|
|
5
|
-
console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, context.error);
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Builds the frontend-control bundle.
|
|
9
|
-
*
|
|
10
|
-
* @param options - Capabilities to expose, how to find the bind token, and the gate to apply.
|
|
11
|
-
* @returns An HTTP extension, tool registrations, and a run-start bind hook — never the registry.
|
|
12
|
-
* @throws Never at construction. A duplicate capability id throws later, when the registrations are
|
|
13
|
-
* registered, naming the id (`ToolRegistry.register`'s own error).
|
|
14
|
-
*
|
|
15
|
-
* @complexity O(n) in `capabilities`.
|
|
16
|
-
*/
|
|
17
|
-
export function createFrontendControl(options) {
|
|
18
|
-
// Closed over, never exposed. See the module doc: reaching this is reaching an ungated `invoke`.
|
|
19
|
-
const registry = createFrontendSessionRegistry();
|
|
20
|
-
const onBindError = options.onBindError ?? defaultBindErrorSink;
|
|
21
|
-
const toolRegistrations = createFrontendCapabilityRegistrations({
|
|
22
|
-
registry,
|
|
23
|
-
capabilities: options.capabilities,
|
|
24
|
-
...(options.policy !== undefined ? { policy: options.policy } : {}),
|
|
25
|
-
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
26
|
-
...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
|
|
27
|
-
});
|
|
28
|
-
const httpExtension = (app, context) => {
|
|
29
|
-
registerFrontendSessionRoutes(app, { registry }, context.adapter);
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* The error boundary the `onBindError` contract actually needs. A `RunStartHandler` throwing is
|
|
33
|
-
* not a no-op: `@jini-ai/http-kit`'s run-start route catches it, marks the run `failed` and answers
|
|
34
|
-
* 500 — so anything inside this hook that can throw can produce exactly the killed run that
|
|
35
|
-
* contract promises never to cause. `resolveBindToken` is host-supplied and reads an opaque host
|
|
36
|
-
* blob (a `JSON.parse` away from throwing), and the sink itself is host-supplied too. A sink that
|
|
37
|
-
* throws leaves nowhere left to report, so that one is swallowed rather than escalated.
|
|
38
|
-
*/
|
|
39
|
-
const reportBindError = (context) => {
|
|
40
|
-
try {
|
|
41
|
-
onBindError(context);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// Deliberately terminal: the reporting channel is what failed.
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const bindOnStarted = ({ request, run, lifecycle }) => {
|
|
48
|
-
let bindToken;
|
|
49
|
-
try {
|
|
50
|
-
bindToken = options.resolveBindToken(request);
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
reportBindError({ runId: run.id, error });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (bindToken === undefined)
|
|
57
|
-
return;
|
|
58
|
-
let release;
|
|
59
|
-
try {
|
|
60
|
-
release = registry.bindRunByToken(run.id, bindToken);
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
reportBindError({ runId: run.id, error });
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
// Release on terminal so a tab that outlives many runs does not accumulate bindings for runs
|
|
67
|
-
// that ended. `waitForTerminal` rejecting is itself a reason to release, not to keep waiting,
|
|
68
|
-
// so both paths run it — and the whole chain is swallowed, because a bookkeeping failure must
|
|
69
|
-
// never surface as an unhandled rejection that takes the process down.
|
|
70
|
-
void lifecycle
|
|
71
|
-
.waitForTerminal(run.id)
|
|
72
|
-
.then(release, release)
|
|
73
|
-
.catch(() => undefined);
|
|
74
|
-
};
|
|
75
|
-
return { httpExtension, toolRegistrations, bindOnStarted };
|
|
76
|
-
}
|
|
77
|
-
//# sourceMappingURL=frontend-control.js.map
|