@jini-ai/daemon 0.6.17 → 0.6.22
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 +10 -0
- package/dist/agent-executor/claude/mcp-config.d.ts +17 -0
- package/dist/agent-executor/claude/mcp-config.js +29 -0
- package/dist/agent-executor/codex/home-config.d.ts +7 -6
- package/dist/agent-executor/codex/home-config.js +15 -3
- package/dist/agent-executor/codex/home-staging.d.ts +4 -2
- package/dist/agent-executor/codex/home-staging.js +5 -3
- package/dist/agent-executor/contracts.d.ts +15 -0
- package/dist/agent-executor/event-translation.js +3 -0
- package/dist/agent-executor/events.d.ts +6 -4
- package/dist/agent-executor/events.js +29 -7
- package/dist/agent-executor/gemini/tool-calls.d.ts +7 -0
- package/dist/agent-executor/gemini/tool-calls.js +9 -0
- package/dist/agent-executor/http-mcp.d.ts +23 -0
- package/dist/agent-executor/http-mcp.js +74 -0
- package/dist/agent-executor/launch.js +43 -11
- package/dist/agent-executor/mcp-bridge.d.ts +10 -2
- package/dist/agent-executor/mcp-bridge.js +2 -2
- package/dist/agent-executor/opencode/mcp-config.d.ts +10 -0
- package/dist/agent-executor/opencode/mcp-config.js +11 -0
- package/dist/agent-executor/opencode/tool-calls.d.ts +7 -0
- package/dist/agent-executor/opencode/tool-calls.js +12 -0
- package/dist/agent-executor/sanitized-line-stream.d.ts +24 -0
- package/dist/agent-executor/sanitized-line-stream.js +45 -0
- package/dist/agent-executor/tool-call-receipts.d.ts +7 -0
- package/dist/agent-executor/tool-call-receipts.js +9 -0
- package/dist/core/read-only-tools.d.ts +2 -0
- package/dist/core/read-only-tools.js +19 -3
- package/dist/delegated-tool-bridge.js +6 -1
- package/dist/http/attachments.d.ts +2 -101
- package/dist/http/attachments.js +13 -1
- package/dist/http/connectors.js +3 -1
- package/dist/http/db-ops.js +3 -1
- package/dist/http/delegated-tools.d.ts +13 -1
- package/dist/http/delegated-tools.js +23 -6
- package/dist/http/frontend-control.d.ts +0 -36
- package/dist/http/frontend-control.js +39 -1
- package/dist/http/frontend-sessions.js +4 -2
- package/dist/http/index.d.ts +2 -1
- package/dist/http/index.js +1 -0
- package/dist/http/media.js +3 -1
- package/dist/http/model-proxy.js +4 -2
- package/dist/http/remote-run-events.js +3 -1
- package/dist/http/research.js +3 -2
- package/dist/http/runs.js +3 -1
- package/dist/http/surface-restoration.d.ts +17 -0
- package/dist/http/surface-restoration.js +50 -0
- package/dist/http/terminals.js +3 -1
- package/dist/http/tool-catalog.d.ts +15 -1
- package/dist/http/tool-execution-routes.d.ts +3 -1
- package/dist/http/tool-execution-routes.js +4 -3
- package/dist/http/xai.js +3 -2
- package/dist/idle-monitor.d.ts +29 -0
- package/dist/idle-monitor.js +64 -0
- package/dist/image-prompt-delivery.d.ts +1 -1
- package/dist/image-prompt-delivery.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/legacy-data-migration.js +3 -1
- package/dist/remote-attachment-lookup.d.ts +3 -1
- package/dist/remote-attachment-lookup.js +2 -2
- package/dist/routines/scheduler.js +6 -4
- package/dist/routing-tool-executor.d.ts +6 -1
- package/dist/routing-tool-executor.js +15 -2
- package/dist/run-lifecycle.d.ts +0 -16
- package/dist/run-lifecycle.js +24 -6
- package/dist/surface-exchanges.d.ts +5 -1
- package/dist/surface-exchanges.js +41 -7
- package/dist/surface-restoration.d.ts +38 -0
- package/dist/surface-restoration.js +152 -0
- package/dist/tool-recovery/index.d.ts +1 -0
- package/dist/tool-recovery/index.js +1 -0
- package/dist/tool-recovery/redaction.d.ts +1 -0
- package/dist/tool-recovery/redaction.js +4 -1
- package/dist/tool-recovery/stack.js +2 -1
- package/dist/tool-recovery/surface-lifetime.d.ts +11 -0
- package/dist/tool-recovery/surface-lifetime.js +41 -0
- package/package.json +14 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
1
2
|
/**
|
|
2
3
|
* @module delegated-tools
|
|
3
4
|
*
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
*/
|
|
28
29
|
import { randomUUID } from 'node:crypto';
|
|
29
30
|
import { createApiError } from '@jini-ai/protocol';
|
|
30
|
-
import { batchOutcomeFor, ToolInputError } from '@jini-ai/core';
|
|
31
|
+
import { redactSecrets, batchOutcomeFor, ToolInputError } from '@jini-ai/core';
|
|
31
32
|
import { createDelegatedToolBridge } from '../index.js';
|
|
32
33
|
import { defineJsonRoute, err, mountJsonRoute, ok, validationError } from '@jini-ai/http-kit';
|
|
33
34
|
import * as readOnlyTools from '../core/read-only-tools.js';
|
|
@@ -44,8 +45,9 @@ function checkReadOnlyConstraint(deps, input) {
|
|
|
44
45
|
}
|
|
45
46
|
/** Logs the real failure server-side and returns the generic, correlation-id-bearing public error (SEC-005: never the raw exception). */
|
|
46
47
|
function defaultInternalErrorSink(context) {
|
|
48
|
+
// Exceptions can echo credentials; inspect preserves stacks and codes before shared redaction.
|
|
47
49
|
// eslint-disable-next-line no-console
|
|
48
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
50
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* The host's {@link DelegatedToolsHttpDeps.describeInternalError} text for `context`, or `undefined`
|
|
@@ -62,7 +64,7 @@ function describeSafely(deps, context) {
|
|
|
62
64
|
}
|
|
63
65
|
catch (error) {
|
|
64
66
|
// eslint-disable-next-line no-console
|
|
65
|
-
console.error(`[@jini-ai/http-kit] describeInternalError threw (correlationId=${context.correlationId})`, error);
|
|
67
|
+
console.error(`[@jini-ai/http-kit] describeInternalError threw (correlationId=${context.correlationId})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
66
68
|
return undefined;
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -128,7 +130,7 @@ function parseDelegatedToolExecute(input) {
|
|
|
128
130
|
* vouches for this exact result via `deps.isModelSafeToolFailure` (it has already redacted the text
|
|
129
131
|
* itself), in which case it is a `422 TOOL_EXECUTION_FAILED` carrying the host's text verbatim.
|
|
130
132
|
*/
|
|
131
|
-
function toolExecutionResultToApiResult(deps, { runId, toolId, result }) {
|
|
133
|
+
function toolExecutionResultToApiResult(deps, { runId, toolId, result, recoveryMessage }) {
|
|
132
134
|
switch (result.status) {
|
|
133
135
|
case 'completed': {
|
|
134
136
|
const outcome = batchOutcomeFor({ output: result.output });
|
|
@@ -141,7 +143,7 @@ function toolExecutionResultToApiResult(deps, { runId, toolId, result }) {
|
|
|
141
143
|
return err({ error: createApiError({ code: 'TOOL_OPERATION_DENIED', message: 'this operation was denied during confirmation' }) });
|
|
142
144
|
case 'timed-out':
|
|
143
145
|
case 'cancelled':
|
|
144
|
-
return err({ error: reportInternalError(deps, { source: 'delegated-tool-execute', error: result.status, runId, toolId, status: result.status }) });
|
|
146
|
+
return err({ error: reportInternalError(deps, { source: 'delegated-tool-execute', error: result.status, runId, toolId, ...(recoveryMessage ? { recoveryMessage } : {}), status: result.status }) });
|
|
145
147
|
case 'failed':
|
|
146
148
|
return err({ error: failedResultToApiError(deps, { runId, toolId, result }) });
|
|
147
149
|
default: {
|
|
@@ -161,6 +163,21 @@ function failedResultToApiError(deps, { runId, toolId, result }) {
|
|
|
161
163
|
}
|
|
162
164
|
return reportInternalError(deps, { source: 'delegated-tool-execute', error: result.error ?? result.status, runId, toolId, status: 'failed' });
|
|
163
165
|
}
|
|
166
|
+
/** Read host-owned recovery facts once after interruption. The host supplies the deadline and
|
|
167
|
+
* permission-checked read; a failed read never replaces or retries the original operation.
|
|
168
|
+
* @complexity One optional bounded read; no extra work on successful executions.
|
|
169
|
+
*/
|
|
170
|
+
async function toolExecutionResultWithRecovery(deps, required) {
|
|
171
|
+
const { principal, runId, toolId, result } = required;
|
|
172
|
+
let recoveryMessage;
|
|
173
|
+
if (result.status === 'timed-out' || result.status === 'cancelled') {
|
|
174
|
+
try {
|
|
175
|
+
recoveryMessage = await deps.recoverInterruptedTool?.({ principal, runId, toolId, result });
|
|
176
|
+
}
|
|
177
|
+
catch { /* Recovery cannot change the interrupted write or expose its own failure. */ }
|
|
178
|
+
}
|
|
179
|
+
return toolExecutionResultToApiResult(deps, { runId, toolId, result, ...(recoveryMessage ? { recoveryMessage } : {}) });
|
|
180
|
+
}
|
|
164
181
|
/**
|
|
165
182
|
* `POST /api/delegated-tool-calls` — executes one delegated tool call against an already-started
|
|
166
183
|
* run. Maps `ToolExecutionResult.status` to the HTTP response via `toolExecutionResultToApiResult`
|
|
@@ -222,7 +239,7 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
|
|
|
222
239
|
input: input.input,
|
|
223
240
|
...(signal !== undefined ? { signal } : {}),
|
|
224
241
|
});
|
|
225
|
-
return
|
|
242
|
+
return toolExecutionResultWithRecovery(deps, { principal, runId: input.runId, toolId: input.toolId, result });
|
|
226
243
|
}
|
|
227
244
|
catch (error) {
|
|
228
245
|
if (error instanceof ToolInputError) {
|
|
@@ -1,39 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module frontend-control
|
|
3
|
-
*
|
|
4
|
-
* Assembles the three halves of agent-driven frontend control into one thing a host can wire up as
|
|
5
|
-
* a config entry rather than a checklist.
|
|
6
|
-
*
|
|
7
|
-
* The parts already existed and none of them composed: `@jini-ai/daemon`'s `FrontendSessionRegistry`
|
|
8
|
-
* addresses a run to a surface, this package's own frontend-session routes carry invocations to a
|
|
9
|
-
* browser and answers back, and `createFrontendCapabilityRegistrations` projects a manifest into
|
|
10
|
-
* gated tools. A host had to construct all three, keep them consistent, and remember to bind a run
|
|
11
|
-
* when it started. Every one of those is a chance to get it subtly wrong, and one of them —
|
|
12
|
-
* binding — is a security decision.
|
|
13
|
-
*
|
|
14
|
-
* **Moved here from `@jini-ai/server` (2026-07-31).** It never needed that package: its real
|
|
15
|
-
* dependencies are `core`, `daemon`, `express` and this module's own siblings, all of which
|
|
16
|
-
* `http-kit` already has. The only thing pinning it there was a *type-only* import of
|
|
17
|
-
* `LocalNodeHttpExtension` — so a helper any Express host could use sat behind a package that also
|
|
18
|
-
* pulls in `@jini-ai/sidecar` and `better-sqlite3`. The reference implementation hit exactly this
|
|
19
|
-
* and hand-reimplemented the composition instead. `@jini-ai/server` re-exports it, so existing
|
|
20
|
-
* imports keep working.
|
|
21
|
-
*
|
|
22
|
-
* **This facade never returns the registry**, and that is its most important property. The
|
|
23
|
-
* registry's `invoke` executes a capability on a real user's screen with no policy check, no
|
|
24
|
-
* confirmation, no timeout, and no audit record — it is safe only because the sole thing that can
|
|
25
|
-
* reach it is a `ToolHandler` that `ToolExecutor` already gated. Handing a host the registry so it
|
|
26
|
-
* could "wire something custom" would hand it a bypass, and a bypass that exists gets used. The
|
|
27
|
-
* host gets an HTTP extension, a set of registrations, and a bind hook; none of them can execute a
|
|
28
|
-
* capability directly.
|
|
29
|
-
*
|
|
30
|
-
* **The bind token is not a field on the run DTO.** A run is a neutral kernel noun and a browser
|
|
31
|
-
* surface is not; putting `frontendBindToken` on `RunCreateRequest` would push a browser concept
|
|
32
|
-
* into a protocol every consumer shares, to serve one transport. Instead the host supplies
|
|
33
|
-
* {@link CreateFrontendControlOptions.resolveBindToken}, which reads the token out of whatever
|
|
34
|
-
* envelope that host already uses — `contextRef` is an opaque host blob precisely so hosts can put
|
|
35
|
-
* their own metadata in it. The engine stays neutral and the host keeps one place to look.
|
|
36
|
-
*/
|
|
37
1
|
import type { Express } from 'express';
|
|
38
2
|
import type { FrontendSessionOwner } from '../frontend-session-registry.js';
|
|
39
3
|
import type { FrontendSessionsHttpDeps } from './frontend-sessions.js';
|
|
@@ -1,8 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module frontend-control
|
|
3
|
+
*
|
|
4
|
+
* Assembles the three halves of agent-driven frontend control into one thing a host can wire up as
|
|
5
|
+
* a config entry rather than a checklist.
|
|
6
|
+
*
|
|
7
|
+
* The parts already existed and none of them composed: `@jini-ai/daemon`'s `FrontendSessionRegistry`
|
|
8
|
+
* addresses a run to a surface, this package's own frontend-session routes carry invocations to a
|
|
9
|
+
* browser and answers back, and `createFrontendCapabilityRegistrations` projects a manifest into
|
|
10
|
+
* gated tools. A host had to construct all three, keep them consistent, and remember to bind a run
|
|
11
|
+
* when it started. Every one of those is a chance to get it subtly wrong, and one of them —
|
|
12
|
+
* binding — is a security decision.
|
|
13
|
+
*
|
|
14
|
+
* **Moved here from `@jini-ai/server` (2026-07-31).** It never needed that package: its real
|
|
15
|
+
* dependencies are `core`, `daemon`, `express` and this module's own siblings, all of which
|
|
16
|
+
* `http-kit` already has. The only thing pinning it there was a *type-only* import of
|
|
17
|
+
* `LocalNodeHttpExtension` — so a helper any Express host could use sat behind a package that also
|
|
18
|
+
* pulls in `@jini-ai/sidecar` and `better-sqlite3`. The reference implementation hit exactly this
|
|
19
|
+
* and hand-reimplemented the composition instead. `@jini-ai/server` re-exports it, so existing
|
|
20
|
+
* imports keep working.
|
|
21
|
+
*
|
|
22
|
+
* **This facade never returns the registry**, and that is its most important property. The
|
|
23
|
+
* registry's `invoke` executes a capability on a real user's screen with no policy check, no
|
|
24
|
+
* confirmation, no timeout, and no audit record — it is safe only because the sole thing that can
|
|
25
|
+
* reach it is a `ToolHandler` that `ToolExecutor` already gated. Handing a host the registry so it
|
|
26
|
+
* could "wire something custom" would hand it a bypass, and a bypass that exists gets used. The
|
|
27
|
+
* host gets an HTTP extension, a set of registrations, and a bind hook; none of them can execute a
|
|
28
|
+
* capability directly.
|
|
29
|
+
*
|
|
30
|
+
* **The bind token is not a field on the run DTO.** A run is a neutral kernel noun and a browser
|
|
31
|
+
* surface is not; putting `frontendBindToken` on `RunCreateRequest` would push a browser concept
|
|
32
|
+
* into a protocol every consumer shares, to serve one transport. Instead the host supplies
|
|
33
|
+
* {@link CreateFrontendControlOptions.resolveBindToken}, which reads the token out of whatever
|
|
34
|
+
* envelope that host already uses — `contextRef` is an opaque host blob precisely so hosts can put
|
|
35
|
+
* their own metadata in it. The engine stays neutral and the host keeps one place to look.
|
|
36
|
+
*/
|
|
37
|
+
import { inspect } from 'node:util';
|
|
38
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
39
|
import { createFrontendCapabilityRegistrations, createFrontendSessionRegistry, } from '../index.js';
|
|
2
40
|
import { registerFrontendSessionRoutes } from './frontend-sessions.js';
|
|
3
41
|
function defaultBindErrorSink(context) {
|
|
4
42
|
// eslint-disable-next-line no-console
|
|
5
|
-
console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, context.error);
|
|
43
|
+
console.error(`[@jini-ai/server] could not bind run "${context.runId}" to a frontend surface`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
6
44
|
}
|
|
7
45
|
/**
|
|
8
46
|
* Builds the frontend-control bundle.
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* composition root can make (it is the thing that starts runs), so it calls `registry.bindRun`
|
|
29
29
|
* directly — see `@jini-ai/daemon`'s own module doc for why attach and bind are separate.
|
|
30
30
|
*/
|
|
31
|
+
import { inspect } from 'node:util';
|
|
32
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
31
33
|
import { randomUUID } from 'node:crypto';
|
|
32
34
|
import { createApiError } from '@jini-ai/protocol';
|
|
33
35
|
import { FrontendSessionCapacityError } from '../frontend-session-registry.js';
|
|
@@ -45,12 +47,12 @@ function reportInternalError(deps, source, error) {
|
|
|
45
47
|
deps.onInternalError({ source, error });
|
|
46
48
|
// eslint-disable-next-line no-console
|
|
47
49
|
else
|
|
48
|
-
console.error(`[@jini-ai/http-kit] internal error (frontend-sessions:${source})`, error);
|
|
50
|
+
console.error(`[@jini-ai/http-kit] internal error (frontend-sessions:${source})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
49
51
|
}
|
|
50
52
|
catch (sinkError) {
|
|
51
53
|
// A diagnostic sink must never turn a contained failure into an unhandled rejection of its own.
|
|
52
54
|
// eslint-disable-next-line no-console
|
|
53
|
-
console.error(`[@jini-ai/http-kit] internal error sink failed (frontend-sessions:${source})`, sinkError);
|
|
55
|
+
console.error(`[@jini-ai/http-kit] internal error sink failed (frontend-sessions:${source})`, redactSecrets({ input: inspect(sinkError) }, { redactPii: false }));
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
/**
|
package/dist/http/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type { RoutineDeleteResponse, RoutineHttpDeps, RoutineListResponse, Routi
|
|
|
21
21
|
export { registerRoutineRoutes, routineCreateRoute, routineDeleteRoute, routineGetRoute, routineListRoute, routineRunNowRoute, routineRunsListRoute, routineUpdateRoute, } from './routines.js';
|
|
22
22
|
export type { CreateDaemonDbToolRegistrationsOptions, DaemonDbHttpDeps, DaemonDbInternalErrorContext, DaemonDbOperations, DaemonDbStatusReport, DaemonDbTableInfo, DaemonDbToolRegistrations, DaemonDbVacuumResult, DbIntegrityIssue, DbIntegrityIssueKind, DbIntegrityReport, } from './db-ops.js';
|
|
23
23
|
export { createDaemonDbToolRegistrations, daemonDbInspectRoute, daemonDbVacuumRoute, daemonDbVerifyRoute, DB_INSPECT_TOOL_ID, DB_VACUUM_TOOL_ID, DB_VERIFY_TOOL_ID, denyAllDaemonDbPolicy, registerDaemonDbRoutes, } from './db-ops.js';
|
|
24
|
-
export type { ToolCatalogEntry, ToolCatalogHttpDeps, ToolCatalogQuery, ToolCatalogSearchHit, } from './tool-catalog.js';
|
|
24
|
+
export type { ToolCatalogEntry, ToolCatalogDescribeOptions, ToolCatalogHttpDeps, ToolCatalogQuery, ToolCatalogSearchHit, } from './tool-catalog.js';
|
|
25
25
|
export { registerToolCatalogRoutes, toolCatalogDescribeRoute, toolCatalogSearchRoute, } from './tool-catalog.js';
|
|
26
26
|
export type { ComponentCatalogEntry, ComponentCatalogHttpDeps, ComponentCatalogQuery, ComponentCatalogSearchHit, } from './component-catalog.js';
|
|
27
27
|
export { registerComponentCatalogRoutes, componentCatalogDescribeRoute, componentCatalogSearchRoute, } from './component-catalog.js';
|
|
@@ -58,4 +58,5 @@ export type { RemoteAttachmentLookupOptional, RemoteAttachmentLookupRequired } f
|
|
|
58
58
|
export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
59
59
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
60
60
|
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
61
|
+
export { tryRestoreSurface } from './surface-restoration.js';
|
|
61
62
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -31,4 +31,5 @@ export { ATTACHMENT_LOOKUP_ROUTE_PATH, registerAttachmentLookupRoutes } from './
|
|
|
31
31
|
export { createRemoteAttachmentLookup } from '../remote-attachment-lookup.js';
|
|
32
32
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
33
33
|
export { createFrontendGuestScope } from './frontend-guest-scope.js';
|
|
34
|
+
export { tryRestoreSurface } from './surface-restoration.js';
|
|
34
35
|
//# sourceMappingURL=index.js.map
|
package/dist/http/media.js
CHANGED
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
* itself can throw before ever creating a task — an unregistered `toolId`-shaped programming
|
|
40
40
|
* error, per `MediaDispatchEngine.generate`'s own contract).
|
|
41
41
|
*/
|
|
42
|
+
import { inspect } from 'node:util';
|
|
43
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
42
44
|
import { randomUUID } from 'node:crypto';
|
|
43
45
|
import { createApiError } from '@jini-ai/protocol';
|
|
44
46
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
@@ -49,7 +51,7 @@ const AUDIO_KINDS = new Set(['music', 'speech', 'sfx']);
|
|
|
49
51
|
const SPEECH_FORMATS = new Set(['mp3', 'wav', 'flac', 'aac', 'opus']);
|
|
50
52
|
function defaultInternalErrorSink(context) {
|
|
51
53
|
// eslint-disable-next-line no-console
|
|
52
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
54
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
53
55
|
}
|
|
54
56
|
function reportInternalError(deps, source, error, taskId, ownerRef) {
|
|
55
57
|
const correlationId = randomUUID();
|
package/dist/http/model-proxy.js
CHANGED
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
* over routing for the five providers that already have a dedicated, more
|
|
82
82
|
* specifically-typed path.
|
|
83
83
|
*/
|
|
84
|
+
import { inspect } from 'node:util';
|
|
85
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
84
86
|
import { randomUUID } from 'node:crypto';
|
|
85
87
|
import { runAnthropicToolTurn, runAzureToolTurn, runGoogleToolTurn, runOllamaToolTurn, runOpenAiToolTurn, } from '@jini-ai/agent-runtime';
|
|
86
88
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -92,7 +94,7 @@ import { err, ok } from '@jini-ai/http-kit';
|
|
|
92
94
|
import { firstOptionalFieldError, isArray, isNumber, isRecord, isString, nonEmptyString, pickDefined, requiredFieldError, } from './model-proxy-fields.js';
|
|
93
95
|
function defaultInternalErrorSink(context) {
|
|
94
96
|
// eslint-disable-next-line no-console
|
|
95
|
-
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, context.error);
|
|
97
|
+
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${context.provider}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
96
98
|
}
|
|
97
99
|
const TOOLS_CHECK = ['tools', isArray, 'tools must be an array when provided'];
|
|
98
100
|
const SYSTEM_CHECK = ['system', isString, 'system must be a string when provided'];
|
|
@@ -382,7 +384,7 @@ function reportCatchAllError(providerParam, onInternalError, error) {
|
|
|
382
384
|
}
|
|
383
385
|
else {
|
|
384
386
|
// eslint-disable-next-line no-console
|
|
385
|
-
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, error);
|
|
387
|
+
console.error(`[@jini-ai/http-kit] internal error (model-proxy/${providerParam || 'unknown'}, correlationId=${correlationId})`, redactSecrets({ input: inspect(error) }, { redactPii: false }));
|
|
386
388
|
}
|
|
387
389
|
return correlationId;
|
|
388
390
|
}
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* would also present, and this route grants direct run-event-injection, a materially different
|
|
23
23
|
* capability from what the general API token was designed to gate.
|
|
24
24
|
*/
|
|
25
|
+
import { inspect } from 'node:util';
|
|
26
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
25
27
|
import { randomUUID } from 'node:crypto';
|
|
26
28
|
import { createApiError, isTerminalRunState } from '@jini-ai/protocol';
|
|
27
29
|
import { defineJsonRoute, mountJsonRoute } from '@jini-ai/http-kit';
|
|
@@ -31,7 +33,7 @@ import { sendApiError, statusForError } from '@jini-ai/http-kit';
|
|
|
31
33
|
import { err, ok } from '@jini-ai/http-kit';
|
|
32
34
|
function defaultInternalErrorSink(context) {
|
|
33
35
|
// eslint-disable-next-line no-console
|
|
34
|
-
console.error(`[@jini-ai/http-kit] internal error (remote-run-events/${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
36
|
+
console.error(`[@jini-ai/http-kit] internal error (remote-run-events/${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
35
37
|
}
|
|
36
38
|
const DEFAULT_TOKEN_ENV_VAR = 'JINI_REMOTE_TOOL_BRIDGE_TOKEN';
|
|
37
39
|
/**
|
package/dist/http/research.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { redactSecrets } from '@jini-ai/core';
|
|
2
1
|
/**
|
|
3
2
|
* @module research
|
|
4
3
|
*
|
|
@@ -95,6 +94,8 @@ import { redactSecrets } from '@jini-ai/core';
|
|
|
95
94
|
* token out of any upstream error text before it is even logged, belt-and-braces alongside the
|
|
96
95
|
* generic-message substitution.
|
|
97
96
|
*/
|
|
97
|
+
import { inspect } from 'node:util';
|
|
98
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
98
99
|
import { randomUUID } from 'node:crypto';
|
|
99
100
|
import { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
100
101
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -103,7 +104,7 @@ import { defineJsonRoute, err, mountJsonRoute, ok, validationError } from '@jini
|
|
|
103
104
|
const MAX_QUERY_LENGTH = 1000;
|
|
104
105
|
function defaultInternalErrorSink(context) {
|
|
105
106
|
// eslint-disable-next-line no-console
|
|
106
|
-
console.error(`[@jini-ai/http-kit] internal error (research/search, correlationId=${context.correlationId})`, context.error);
|
|
107
|
+
console.error(`[@jini-ai/http-kit] internal error (research/search, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
107
108
|
}
|
|
108
109
|
/**
|
|
109
110
|
* Default `ResearchHttpDeps.resolveCredentials`: reads `TAVILY_API_KEY` from the environment.
|
package/dist/http/runs.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* agent, tool, or product vocabulary: a host optionally supplies `onStarted`
|
|
4
4
|
* to attach its chosen driver after the lifecycle has durably recorded start.
|
|
5
5
|
*/
|
|
6
|
+
import { inspect } from 'node:util';
|
|
7
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
6
8
|
import { randomUUID } from 'node:crypto';
|
|
7
9
|
import { createApiError } from '@jini-ai/protocol';
|
|
8
10
|
import { createSseChannel, defineJsonRoute, err, mountJsonRoute, ok, requestedAfterCursor, sendApiError, sendRawApiError, validationError, } from '@jini-ai/http-kit';
|
|
9
11
|
/** Default sink when a host does not supply `onInternalError`: still observable, never silent. */
|
|
10
12
|
function defaultInternalErrorSink(context) {
|
|
11
13
|
// eslint-disable-next-line no-console
|
|
12
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
14
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Logs the real failure server-side and returns the generic, correlation-id-bearing public error
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { SurfaceExchangeStore } from '@jini-ai/core';
|
|
3
|
+
/** Stream a restored card through the existing authenticated callback route. Only the exchange
|
|
4
|
+
* handle is accepted: tool identity, principal and original arguments come from the shared store.
|
|
5
|
+
* Returns false without writing a response when another process may own the exchange.
|
|
6
|
+
* @complexity O(emitted bytes); no answer or credential is retained by this adapter.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tryRestoreSurface({ res, store, exchangeId, principalId, toolId }: {
|
|
9
|
+
res: Response;
|
|
10
|
+
store: SurfaceExchangeStore;
|
|
11
|
+
exchangeId: string;
|
|
12
|
+
principalId: string;
|
|
13
|
+
toolId: string;
|
|
14
|
+
}, { params }?: {
|
|
15
|
+
params?: Record<string, unknown>;
|
|
16
|
+
}): Promise<boolean>;
|
|
17
|
+
//# sourceMappingURL=surface-restoration.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Stream a restored card through the existing authenticated callback route. Only the exchange
|
|
2
|
+
* handle is accepted: tool identity, principal and original arguments come from the shared store.
|
|
3
|
+
* Returns false without writing a response when another process may own the exchange.
|
|
4
|
+
* @complexity O(emitted bytes); no answer or credential is retained by this adapter.
|
|
5
|
+
*/
|
|
6
|
+
export async function tryRestoreSurface({ res, store, exchangeId, principalId, toolId }, { params } = {}) {
|
|
7
|
+
if (!store.restore)
|
|
8
|
+
return false;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const onClose = () => { if (!res.writableFinished)
|
|
11
|
+
controller.abort(); };
|
|
12
|
+
res.on('close', onClose);
|
|
13
|
+
const write = (value) => {
|
|
14
|
+
if (res.destroyed)
|
|
15
|
+
return;
|
|
16
|
+
if (!res.headersSent)
|
|
17
|
+
res.status(200).set({ 'content-type': 'application/x-ndjson', 'cache-control': 'no-store' });
|
|
18
|
+
res.write(`${JSON.stringify(value)}\n`);
|
|
19
|
+
};
|
|
20
|
+
// The first card frame opens the response. Subsequent heartbeats keep Node fetch's idle-body
|
|
21
|
+
// limit from expiring a legitimate human wait at the proxy hop.
|
|
22
|
+
const heartbeat = setInterval(() => { if (res.headersSent)
|
|
23
|
+
write({ type: 'keepalive' }); }, 15_000);
|
|
24
|
+
heartbeat.unref();
|
|
25
|
+
try {
|
|
26
|
+
const result = await store.restore({ exchangeId, principalId, emit: async (emission) => {
|
|
27
|
+
write({ type: 'surface', emission });
|
|
28
|
+
} }, { toolId, signal: controller.signal, ...(params === undefined ? {} : { params }) });
|
|
29
|
+
if (!result.ok && result.reason === 'unknown-or-closed' && !res.headersSent)
|
|
30
|
+
return false;
|
|
31
|
+
if (!result.ok && !res.headersSent) {
|
|
32
|
+
res.status(409).json({ code: 'SURFACE_NOT_PENDING', reason: result.reason });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
write({ type: 'restored', ...result });
|
|
36
|
+
res.end();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Failure messages may contain host or credential details. Restoration never reports them.
|
|
41
|
+
write({ type: 'restored', ok: false });
|
|
42
|
+
res.end();
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearInterval(heartbeat);
|
|
47
|
+
res.off('close', onClose);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=surface-restoration.js.map
|
package/dist/http/terminals.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* - Adapts `deps.manager.attach`'s push/end sink to `sse.ts`'s generic SSE
|
|
26
26
|
* channel, the same way `runs.ts` adapts `RunLifecycle.stream`.
|
|
27
27
|
*/
|
|
28
|
+
import { inspect } from 'node:util';
|
|
29
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
28
30
|
import { randomUUID } from 'node:crypto';
|
|
29
31
|
import { TERMINAL_CREATE_TOOL_ID } from '../index.js';
|
|
30
32
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -38,7 +40,7 @@ import { isRecord, readOptionalNonEmptyString, readOptionalNumber, resolveResour
|
|
|
38
40
|
import { err, ok } from '@jini-ai/http-kit';
|
|
39
41
|
function defaultTerminalsInternalErrorSink(context) {
|
|
40
42
|
// eslint-disable-next-line no-console
|
|
41
|
-
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
43
|
+
console.error(`[@jini-ai/http-kit] internal error (${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
42
44
|
}
|
|
43
45
|
/** Every one-off `terminal.create` tool call gets its own opaque `RunRef` — matching `db-ops.ts`'s `freshRun()`: these are single requests, not steps of a longer-lived agent run. */
|
|
44
46
|
function freshRun() {
|
|
@@ -36,6 +36,20 @@ export interface ToolCatalogEntry {
|
|
|
36
36
|
readonly description: string;
|
|
37
37
|
readonly inputSchema?: unknown;
|
|
38
38
|
readonly source: string;
|
|
39
|
+
/**
|
|
40
|
+
* Set only on guidance entries that are not tools themselves (a retired-alias pointer, a
|
|
41
|
+
* namespace overview): every tool id the guidance points at. A read-only view shows guidance
|
|
42
|
+
* only when each target is a registered read-only tool, and hides guidance without targets.
|
|
43
|
+
*/
|
|
44
|
+
readonly targetToolIds?: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
/** Optional describe inputs. */
|
|
47
|
+
export interface ToolCatalogDescribeOptions {
|
|
48
|
+
/**
|
|
49
|
+
* When set, the caller may use only these tool ids; guidance that lists tools (a namespace
|
|
50
|
+
* overview) should list only these. Never consulted for a registered tool's own entry.
|
|
51
|
+
*/
|
|
52
|
+
readonly visibleToolIds?: ReadonlySet<string>;
|
|
39
53
|
}
|
|
40
54
|
/** Structurally identical to `@jini-ai/sqlite-chat`'s `searchToolCatalog`/`getToolCatalogEntry` — defined locally so this package incurs no `better-sqlite3` dependency. */
|
|
41
55
|
export interface ToolCatalogQuery {
|
|
@@ -46,7 +60,7 @@ export interface ToolCatalogQuery {
|
|
|
46
60
|
}) => readonly ToolCatalogSearchHit[];
|
|
47
61
|
readonly describe: ({ id }: {
|
|
48
62
|
readonly id: string;
|
|
49
|
-
},
|
|
63
|
+
}, optional?: ToolCatalogDescribeOptions) => ToolCatalogEntry | null;
|
|
50
64
|
}
|
|
51
65
|
export interface ToolCatalogHttpDeps {
|
|
52
66
|
readonly catalog: ToolCatalogQuery;
|
|
@@ -57,6 +57,8 @@ export interface ToolExecutionRoutesOptional {
|
|
|
57
57
|
/** Defaults to {@link DEFAULT_TOOL_EXEC_MAX_BODY_BYTES}. */
|
|
58
58
|
readonly maxBodyBytes?: number;
|
|
59
59
|
readonly runWithContext?: RunWithToolContext;
|
|
60
|
+
/** Host-owned live-run/identity gate, after parsing and before either route dispatches. */
|
|
61
|
+
readonly authorizeRun?: RequestHandler;
|
|
60
62
|
}
|
|
61
63
|
/** The token + loopback gate; mounted before any body parser. Exported for hosts that need it on another path. */
|
|
62
64
|
export declare function requireToolExecutionCaller(_required: Record<string, never>, { tokenEnvVar, env }?: Pick<ToolExecutionRoutesOptional, 'tokenEnvVar' | 'env'>): RequestHandler;
|
|
@@ -65,5 +67,5 @@ export declare function requireToolExecutionCaller(_required: Record<string, nev
|
|
|
65
67
|
* `express.json()` with a smaller limit: this route parses its own body after its gate, and an
|
|
66
68
|
* earlier parser would both buffer unauthenticated bodies and cap tool inputs at its own limit.
|
|
67
69
|
*/
|
|
68
|
-
export declare function registerToolExecutionRoutes({ app, executor, listToolsFor }: ToolExecutionRoutesRequired, { tokenEnvVar, env, maxBodyBytes, runWithContext }?: ToolExecutionRoutesOptional): void;
|
|
70
|
+
export declare function registerToolExecutionRoutes({ app, executor, listToolsFor }: ToolExecutionRoutesRequired, { tokenEnvVar, env, maxBodyBytes, runWithContext, authorizeRun }?: ToolExecutionRoutesOptional): void;
|
|
69
71
|
//# sourceMappingURL=tool-execution-routes.d.ts.map
|
|
@@ -172,10 +172,11 @@ function catalogHandler({ listToolsFor }) {
|
|
|
172
172
|
* `express.json()` with a smaller limit: this route parses its own body after its gate, and an
|
|
173
173
|
* earlier parser would both buffer unauthenticated bodies and cap tool inputs at its own limit.
|
|
174
174
|
*/
|
|
175
|
-
export function registerToolExecutionRoutes({ app, executor, listToolsFor }, { tokenEnvVar, env, maxBodyBytes = DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, runWithContext } = {}) {
|
|
175
|
+
export function registerToolExecutionRoutes({ app, executor, listToolsFor }, { tokenEnvVar, env, maxBodyBytes = DEFAULT_TOOL_EXEC_MAX_BODY_BYTES, runWithContext, authorizeRun } = {}) {
|
|
176
176
|
const gate = requireToolExecutionCaller({}, { ...(tokenEnvVar === undefined ? {} : { tokenEnvVar }), ...(env === undefined ? {} : { env }) });
|
|
177
177
|
const parser = express.json({ limit: maxBodyBytes });
|
|
178
|
-
|
|
179
|
-
app.
|
|
178
|
+
const runGate = authorizeRun ? [authorizeRun] : [];
|
|
179
|
+
app.post(TOOL_EXECUTIONS_ROUTE_PATH, gate, parser, ...runGate, executionHandler({ executor }, runWithContext === undefined ? {} : { runWithContext }), bodyParserFailure);
|
|
180
|
+
app.get(TOOL_CATALOG_ROUTE_PATH, gate, ...runGate, catalogHandler({ listToolsFor }));
|
|
180
181
|
}
|
|
181
182
|
//# sourceMappingURL=tool-execution-routes.js.map
|
package/dist/http/xai.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { redactSecrets } from '@jini-ai/core';
|
|
2
1
|
/**
|
|
3
2
|
* @module xai
|
|
4
3
|
*
|
|
@@ -117,6 +116,8 @@ import { redactSecrets } from '@jini-ai/core';
|
|
|
117
116
|
* `@jini-ai/agent-runtime`'s already-shipped generic module, outside this file's own scope to alter
|
|
118
117
|
* without special-casing one provider's capitalization inside otherwise-generic code.
|
|
119
118
|
*/
|
|
119
|
+
import { inspect } from 'node:util';
|
|
120
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
120
121
|
import { randomUUID } from 'node:crypto';
|
|
121
122
|
import { beginOAuthPkce, clearStoredOAuthToken, completeOAuthPkce, getStoredOAuthToken, PendingAuthCache, resolveOAuthBearer, setStoredOAuthToken, startOAuthCallbackListener, validateBaseUrl, XAI_OAUTH_PROVIDER_CONFIG, XAI_OAUTH_REDIRECT_HOST, XAI_OAUTH_REDIRECT_PATH, XAI_OAUTH_REDIRECT_PORT, } from '@jini-ai/agent-runtime';
|
|
122
123
|
import { createApiError } from '@jini-ai/protocol';
|
|
@@ -137,7 +138,7 @@ const DEFAULT_SEARCH_MODEL = 'grok-4.20-reasoning';
|
|
|
137
138
|
const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
|
|
138
139
|
function defaultInternalErrorSink(context) {
|
|
139
140
|
// eslint-disable-next-line no-console
|
|
140
|
-
console.error(`[@jini-ai/http-kit] internal error (xai/${context.source}, correlationId=${context.correlationId})`, context.error);
|
|
141
|
+
console.error(`[@jini-ai/http-kit] internal error (xai/${context.source}, correlationId=${context.correlationId})`, redactSecrets({ input: inspect(context.error) }, { redactPii: false }));
|
|
141
142
|
}
|
|
142
143
|
/** Resolves every optional `XaiHttpDeps` field to a concrete value. Idempotent — calling this again on an already-resolved object (e.g. `registerXaiRoutes`'s own single resolution, passed through to each route's `handle`) is a harmless pass-through. See module doc on why stateful fields (`pending`/`listenerRef`) must be resolved once per registration, not once per request. */
|
|
143
144
|
function resolveXaiHttpDeps(deps) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Clock } from '@jini-ai/core/primitives';
|
|
2
|
+
import type { SchedulerPort } from './scheduler.js';
|
|
3
|
+
/** A one-shot observation lifecycle; stopping permanently cancels further observations. */
|
|
4
|
+
export interface IdleMonitor {
|
|
5
|
+
/** Idempotent; callbacks queued before cancellation become harmless. */
|
|
6
|
+
stop(): void;
|
|
7
|
+
/** Hold busy state through asynchronous work. Null means intake is fenced; releases are idempotent. */
|
|
8
|
+
acquire(required: Record<string, never>, optional?: Record<string, never>): (() => void) | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Observe host busy state immediately, scheduling polls at most 1s ahead; fire once after a full
|
|
12
|
+
* observed idle interval. Busy observations discard idle credit; the next idle
|
|
13
|
+
* observation starts a new interval. acquire() also records activity between observations.
|
|
14
|
+
* @param input Injected synchronous busy predicate, finite positive idleMs, clock and scheduler.
|
|
15
|
+
* @param options onIdle runs synchronously after the monitor has stopped; its errors propagate.
|
|
16
|
+
* @returns A permanent, idempotent stop handle. Owns at most one scheduled callback.
|
|
17
|
+
* @throws RangeError for a non-positive or non-finite idleMs, before scheduling.
|
|
18
|
+
* @complexity O(1) time per observation and O(1) retained space.
|
|
19
|
+
* @example createIdleMonitor({ isBusy, idleMs: 900_000, clock, scheduler }, { onIdle });
|
|
20
|
+
*/
|
|
21
|
+
export declare function createIdleMonitor({ isBusy, idleMs, clock, scheduler }: {
|
|
22
|
+
readonly isBusy: () => boolean;
|
|
23
|
+
readonly idleMs: number;
|
|
24
|
+
readonly clock: Clock;
|
|
25
|
+
readonly scheduler: SchedulerPort;
|
|
26
|
+
}, { onIdle }: {
|
|
27
|
+
readonly onIdle: () => void;
|
|
28
|
+
}): IdleMonitor;
|
|
29
|
+
//# sourceMappingURL=idle-monitor.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe host busy state immediately, scheduling polls at most 1s ahead; fire once after a full
|
|
3
|
+
* observed idle interval. Busy observations discard idle credit; the next idle
|
|
4
|
+
* observation starts a new interval. acquire() also records activity between observations.
|
|
5
|
+
* @param input Injected synchronous busy predicate, finite positive idleMs, clock and scheduler.
|
|
6
|
+
* @param options onIdle runs synchronously after the monitor has stopped; its errors propagate.
|
|
7
|
+
* @returns A permanent, idempotent stop handle. Owns at most one scheduled callback.
|
|
8
|
+
* @throws RangeError for a non-positive or non-finite idleMs, before scheduling.
|
|
9
|
+
* @complexity O(1) time per observation and O(1) retained space.
|
|
10
|
+
* @example createIdleMonitor({ isBusy, idleMs: 900_000, clock, scheduler }, { onIdle });
|
|
11
|
+
*/
|
|
12
|
+
export function createIdleMonitor({ isBusy, idleMs, clock, scheduler }, { onIdle }) {
|
|
13
|
+
if (!Number.isFinite(idleMs) || idleMs <= 0) {
|
|
14
|
+
throw new RangeError('idleMs must be finite and positive');
|
|
15
|
+
}
|
|
16
|
+
let stopped = false;
|
|
17
|
+
let active = 0;
|
|
18
|
+
let idleSince;
|
|
19
|
+
let cancelPending;
|
|
20
|
+
const stop = () => {
|
|
21
|
+
stopped = true;
|
|
22
|
+
cancelPending?.();
|
|
23
|
+
cancelPending = undefined;
|
|
24
|
+
};
|
|
25
|
+
// The host owns parking; this callback only observes state and reports idle.
|
|
26
|
+
const observe = () => {
|
|
27
|
+
if (stopped)
|
|
28
|
+
return;
|
|
29
|
+
const now = clock.nowMs();
|
|
30
|
+
if (active > 0 || isBusy())
|
|
31
|
+
idleSince = undefined;
|
|
32
|
+
else
|
|
33
|
+
idleSince ??= now;
|
|
34
|
+
const remainingMs = idleSince === undefined ? idleMs : idleMs - (now - idleSince);
|
|
35
|
+
if (remainingMs <= 0) {
|
|
36
|
+
stop();
|
|
37
|
+
onIdle();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
cancelPending = scheduler.schedule({ delayMs: Math.min(1_000, remainingMs), callback: observe });
|
|
41
|
+
};
|
|
42
|
+
observe();
|
|
43
|
+
return {
|
|
44
|
+
stop,
|
|
45
|
+
acquire(_required, _optional = {}) {
|
|
46
|
+
if (stopped)
|
|
47
|
+
return null;
|
|
48
|
+
active++;
|
|
49
|
+
idleSince = undefined;
|
|
50
|
+
let released = false;
|
|
51
|
+
return () => {
|
|
52
|
+
if (released)
|
|
53
|
+
return;
|
|
54
|
+
released = true;
|
|
55
|
+
active--;
|
|
56
|
+
// A sub-poll request is still activity. Start its idle credit at completion, not at
|
|
57
|
+
// the previous observation, so frequent short calls cannot trigger a premature park.
|
|
58
|
+
if (active === 0)
|
|
59
|
+
idleSince = clock.nowMs();
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=idle-monitor.js.map
|
|
@@ -22,7 +22,7 @@ export interface ImagePromptDelivery {
|
|
|
22
22
|
export declare function augmentPromptWithImageAttachments({ prompt, imagePaths }: {
|
|
23
23
|
readonly prompt: string;
|
|
24
24
|
readonly imagePaths: readonly string[];
|
|
25
|
-
}): string;
|
|
25
|
+
}, _optional?: Record<string, never>): string;
|
|
26
26
|
/**
|
|
27
27
|
* Directories a 'prompt-path' def's CLI must be allowed to read from so it
|
|
28
28
|
* can actually open the files the augmented prompt just told it about — the
|
|
@@ -45,13 +45,14 @@ import path from 'node:path';
|
|
|
45
45
|
* @complexity O(n) in the number of images — one line rendered per path, no I/O.
|
|
46
46
|
* @overallScore 100
|
|
47
47
|
*/
|
|
48
|
-
export function augmentPromptWithImageAttachments({ prompt, imagePaths }) {
|
|
48
|
+
export function augmentPromptWithImageAttachments({ prompt, imagePaths }, _optional = {}) {
|
|
49
49
|
const list = imagePaths.map((imagePath, index) => `${index + 1}. ${imagePath}`).join('\n');
|
|
50
50
|
const plural = imagePaths.length === 1;
|
|
51
51
|
return (`${prompt}\n\n---\n` +
|
|
52
52
|
`The user attached the following image ${plural ? 'file' : 'files'} to this message. ` +
|
|
53
|
-
`${plural ? 'It is a' : 'These are'} real file${plural ? '' : 's'} on disk — read ${plural ? 'it' : 'each one'}
|
|
54
|
-
`
|
|
53
|
+
`${plural ? 'It is a' : 'These are'} real file${plural ? '' : 's'} on disk — read ${plural ? 'it' : 'each one'} ` +
|
|
54
|
+
`with the host attachment/image tool first, or your native Read/image tool for this supplied path. ` +
|
|
55
|
+
`Never use Bash, a shell command or a script to inspect attachments:\n${list}`);
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
57
58
|
* Directories a 'prompt-path' def's CLI must be allowed to read from so it
|
package/dist/index.d.ts
CHANGED