@north-light/crouter 0.3.217 → 0.3.219
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/api/client.d.ts +5 -2
- package/dist/api/client.js +7 -1
- package/dist/api/dto/canvas.d.ts +9 -0
- package/dist/api/dto/nodes.d.ts +27 -1
- package/dist/builtin-memory/04-base-worker.md +7 -1
- package/dist/builtin-memory/internal/plugins.md +67 -0
- package/dist/builtin-memory/memory-read-orientation.md +13 -0
- package/dist/clients/attach/chrome/bash-jobs.js +1 -1
- package/dist/clients/attach/photon_rs_bg.wasm +0 -0
- package/dist/clients/attach/viewer.js +478 -478
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/memory/edit.js +6 -1
- package/dist/commands/memory/lint.d.ts +1 -6
- package/dist/commands/memory/lint.js +18 -126
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +17 -2
- package/dist/commands/memory/read.js +15 -6
- package/dist/commands/memory/shared.d.ts +32 -2
- package/dist/commands/memory/shared.js +179 -0
- package/dist/commands/memory/write.js +6 -1
- package/dist/commands/pkg/browse/catalog.js +2 -0
- package/dist/commands/pkg/browse/model.d.ts +4 -1
- package/dist/commands/pkg/plugin-manage.js +209 -148
- package/dist/core/bash-jobs.d.ts +2 -5
- package/dist/core/bash-jobs.js +4 -8
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +16 -2
- package/dist/core/human/component-docs.js +1 -0
- package/dist/core/human/scan.d.ts +5 -4
- package/dist/core/human/scan.js +7 -4
- package/dist/core/io.js +3 -2
- package/dist/core/manifest.d.ts +2 -0
- package/dist/core/manifest.js +5 -0
- package/dist/core/memory/extensions.d.ts +30 -0
- package/dist/core/memory/extensions.js +219 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +25 -0
- package/dist/core/runtime/launch-config-worker.d.ts +1 -0
- package/dist/core/runtime/launch-config-worker.js +12 -0
- package/dist/core/runtime/launch.d.ts +16 -2
- package/dist/core/runtime/launch.js +55 -21
- package/dist/core/runtime/promote.d.ts +20 -2
- package/dist/core/runtime/promote.js +63 -59
- package/dist/core/runtime/recycle.js +2 -2
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/spawn.js +3 -3
- package/dist/core/runtime/warm-pool.js +9 -7
- package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
- package/dist/core/substrate/frontmatter-validation.js +101 -0
- package/dist/core/substrate/index.d.ts +1 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +30 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +30 -0
- package/dist/daemon/api/bridge.d.ts +6 -0
- package/dist/daemon/api/bridge.js +39 -2
- package/dist/daemon/api/handlers/canvas.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +4 -3
- package/dist/daemon/api/handlers/nodes.d.ts +1 -0
- package/dist/daemon/api/handlers/nodes.js +117 -36
- package/dist/daemon/api/handlers/reports.d.ts +4 -0
- package/dist/daemon/api/handlers/reports.js +14 -8
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/manage.d.ts +3 -0
- package/dist/daemon/manage.js +14 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
- package/dist/pi-extensions/canvas-bash-valve.js +19 -6
- package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
- package/dist/pi-extensions/canvas-preview-result.js +9 -23
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/core/manifest.d.ts
CHANGED
|
@@ -2,4 +2,6 @@ import type { MarketplaceManifest, PluginManifest } from '../types.js';
|
|
|
2
2
|
export declare function pluginManifestPath(pluginRoot: string): string;
|
|
3
3
|
export declare function marketplaceManifestPath(mktRoot: string): string;
|
|
4
4
|
export declare function readPluginManifest(pluginRoot: string): PluginManifest | null;
|
|
5
|
+
/** Validate an optional memory-extension declaration at package preflight. */
|
|
6
|
+
export declare function validatePluginManifestMemoryExtensions(manifest: PluginManifest): void;
|
|
5
7
|
export declare function readMarketplaceManifest(mktRoot: string): MarketplaceManifest | null;
|
package/dist/core/manifest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { MARKETPLACE_MANIFEST_DIR, MARKETPLACE_MANIFEST_FILE, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE, } from '../types.js';
|
|
3
|
+
import { validatePluginMemoryExtensions } from './memory/extensions.js';
|
|
3
4
|
import { readJsonIfExists } from './fs-utils.js';
|
|
4
5
|
export function pluginManifestPath(pluginRoot) {
|
|
5
6
|
return join(pluginRoot, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
@@ -10,6 +11,10 @@ export function marketplaceManifestPath(mktRoot) {
|
|
|
10
11
|
export function readPluginManifest(pluginRoot) {
|
|
11
12
|
return readJsonIfExists(pluginManifestPath(pluginRoot));
|
|
12
13
|
}
|
|
14
|
+
/** Validate an optional memory-extension declaration at package preflight. */
|
|
15
|
+
export function validatePluginManifestMemoryExtensions(manifest) {
|
|
16
|
+
validatePluginMemoryExtensions(manifest);
|
|
17
|
+
}
|
|
13
18
|
export function readMarketplaceManifest(mktRoot) {
|
|
14
19
|
return readJsonIfExists(marketplaceManifestPath(mktRoot));
|
|
15
20
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MemoryExtensionDeclarations, MemoryExtensionScalar, PluginManifest } from '../../types.js';
|
|
2
|
+
import type { MemoryDoc } from '../memory-resolver.js';
|
|
3
|
+
export type MemoryExtensionCatalog = Record<string, MemoryExtensionDeclarations>;
|
|
4
|
+
export interface MemoryExtensionValidationIssue {
|
|
5
|
+
path: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MemoryExtensionCatalogOptions {
|
|
9
|
+
/** The manifest currently being staged. Its namespace shadows an installed copy only for that candidate validation. */
|
|
10
|
+
candidate?: Pick<PluginManifest, 'name' | 'memory_extensions'>;
|
|
11
|
+
}
|
|
12
|
+
export declare function isMemoryExtensionScalar(value: unknown): value is MemoryExtensionScalar;
|
|
13
|
+
/** Parse the closed declaration addendum. Callers that accept a plugin manifest
|
|
14
|
+
* invoke this at their install/preflight boundary; normal runtime catalogs only
|
|
15
|
+
* admit declarations which have already cleared that boundary. */
|
|
16
|
+
export declare function validateMemoryExtensionDeclarations(raw: unknown): MemoryExtensionDeclarations;
|
|
17
|
+
/** Throw when a manifest carries an invalid addendum; absent is valid. */
|
|
18
|
+
export declare function validatePluginMemoryExtensions(manifest: Pick<PluginManifest, 'memory_extensions'>): void;
|
|
19
|
+
/** Installed declarations used for validation and authoring. Disabled plugins
|
|
20
|
+
* remain present so existing values can still be checked and edited. */
|
|
21
|
+
export declare function memoryExtensionValidationCatalog(doc: Pick<MemoryDoc, 'scope' | 'path'>, options?: MemoryExtensionCatalogOptions): MemoryExtensionCatalog;
|
|
22
|
+
/** Winning enabled declarations used only to interpret structured output. */
|
|
23
|
+
export declare function memoryExtensionEffectiveCatalog(doc: Pick<MemoryDoc, 'scope' | 'path'>): MemoryExtensionCatalog;
|
|
24
|
+
/** Validate the raw `extensions` frontmatter value. Unresolved namespaces are
|
|
25
|
+
* intentionally reported rather than discarded: removal leaves raw metadata
|
|
26
|
+
* intact but lint-invalid until its plugin returns. */
|
|
27
|
+
export declare function validateMemoryExtensionValues(raw: unknown, catalog: MemoryExtensionCatalog): MemoryExtensionValidationIssue[];
|
|
28
|
+
/** Effective plugin metadata for structured consumers. It overlays only valid
|
|
29
|
+
* explicit scalars onto defaults and never mutates or serializes frontmatter. */
|
|
30
|
+
export declare function projectEffectiveMemoryExtensions(raw: unknown, catalog: MemoryExtensionCatalog): Record<string, Record<string, MemoryExtensionScalar>>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { sep } from 'node:path';
|
|
2
|
+
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
|
|
3
|
+
const LOCAL_FIELD_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
4
|
+
const DECLARATION_KEYS = new Set(['type', 'values', 'default', 'write_help', 'edit_help']);
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
export function isMemoryExtensionScalar(value) {
|
|
9
|
+
return typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value));
|
|
10
|
+
}
|
|
11
|
+
function declarationError(path, message) {
|
|
12
|
+
return new Error(`invalid memory_extensions declaration at ${path}: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
function validHelp(value) {
|
|
15
|
+
return typeof value === 'string' && value.trim() !== '' && !/[\r\n]/.test(value);
|
|
16
|
+
}
|
|
17
|
+
function validDefault(declaration, value) {
|
|
18
|
+
if (!isMemoryExtensionScalar(value))
|
|
19
|
+
return false;
|
|
20
|
+
if (declaration.type === 'enum')
|
|
21
|
+
return typeof value === 'string' && declaration.values.includes(value);
|
|
22
|
+
return typeof value === declaration.type;
|
|
23
|
+
}
|
|
24
|
+
/** Parse the closed declaration addendum. Callers that accept a plugin manifest
|
|
25
|
+
* invoke this at their install/preflight boundary; normal runtime catalogs only
|
|
26
|
+
* admit declarations which have already cleared that boundary. */
|
|
27
|
+
export function validateMemoryExtensionDeclarations(raw) {
|
|
28
|
+
if (!isRecord(raw))
|
|
29
|
+
throw declarationError('memory_extensions', 'expected a non-empty object');
|
|
30
|
+
const declarations = {};
|
|
31
|
+
const fields = Object.entries(raw);
|
|
32
|
+
if (fields.length === 0)
|
|
33
|
+
throw declarationError('memory_extensions', 'must not be empty');
|
|
34
|
+
for (const [field, value] of fields) {
|
|
35
|
+
const path = `memory_extensions.${field}`;
|
|
36
|
+
if (!LOCAL_FIELD_NAME.test(field)) {
|
|
37
|
+
throw declarationError(path, 'field names must be lowercase kebab-case');
|
|
38
|
+
}
|
|
39
|
+
if (!isRecord(value))
|
|
40
|
+
throw declarationError(path, 'expected a declaration object');
|
|
41
|
+
for (const key of Object.keys(value)) {
|
|
42
|
+
if (!DECLARATION_KEYS.has(key))
|
|
43
|
+
throw declarationError(path, `unknown key \`${key}\``);
|
|
44
|
+
}
|
|
45
|
+
const type = value['type'];
|
|
46
|
+
if (type !== 'boolean' && type !== 'string' && type !== 'number' && type !== 'enum') {
|
|
47
|
+
throw declarationError(`${path}.type`, 'expected boolean, string, number, or enum');
|
|
48
|
+
}
|
|
49
|
+
if (!validHelp(value['write_help']))
|
|
50
|
+
throw declarationError(`${path}.write_help`, 'expected a nonblank single-line string');
|
|
51
|
+
if (!validHelp(value['edit_help']))
|
|
52
|
+
throw declarationError(`${path}.edit_help`, 'expected a nonblank single-line string');
|
|
53
|
+
if (type === 'enum') {
|
|
54
|
+
const values = value['values'];
|
|
55
|
+
if (!Array.isArray(values) || values.length === 0 || !values.every((entry) => typeof entry === 'string' && entry !== '')) {
|
|
56
|
+
throw declarationError(`${path}.values`, 'expected a non-empty string list');
|
|
57
|
+
}
|
|
58
|
+
if (new Set(values).size !== values.length)
|
|
59
|
+
throw declarationError(`${path}.values`, 'values must be unique');
|
|
60
|
+
const declaration = {
|
|
61
|
+
type,
|
|
62
|
+
values: [...values],
|
|
63
|
+
write_help: value['write_help'],
|
|
64
|
+
edit_help: value['edit_help'],
|
|
65
|
+
};
|
|
66
|
+
if ('default' in value) {
|
|
67
|
+
if (!validDefault(declaration, value['default'])) {
|
|
68
|
+
throw declarationError(`${path}.default`, 'must be one of the declared enum values');
|
|
69
|
+
}
|
|
70
|
+
declaration.default = value['default'];
|
|
71
|
+
}
|
|
72
|
+
declarations[field] = declaration;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if ('values' in value)
|
|
76
|
+
throw declarationError(`${path}.values`, 'is allowed only for enum declarations');
|
|
77
|
+
const declaration = {
|
|
78
|
+
type,
|
|
79
|
+
write_help: value['write_help'],
|
|
80
|
+
edit_help: value['edit_help'],
|
|
81
|
+
};
|
|
82
|
+
if ('default' in value) {
|
|
83
|
+
if (!validDefault(declaration, value['default'])) {
|
|
84
|
+
throw declarationError(`${path}.default`, `must be a ${type}`);
|
|
85
|
+
}
|
|
86
|
+
declaration.default = value['default'];
|
|
87
|
+
}
|
|
88
|
+
declarations[field] = declaration;
|
|
89
|
+
}
|
|
90
|
+
return declarations;
|
|
91
|
+
}
|
|
92
|
+
/** Throw when a manifest carries an invalid addendum; absent is valid. */
|
|
93
|
+
export function validatePluginMemoryExtensions(manifest) {
|
|
94
|
+
if (manifest.memory_extensions !== undefined)
|
|
95
|
+
validateMemoryExtensionDeclarations(manifest.memory_extensions);
|
|
96
|
+
}
|
|
97
|
+
function declarationsFromManifest(manifest, strict) {
|
|
98
|
+
if (manifest.memory_extensions === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
try {
|
|
101
|
+
return validateMemoryExtensionDeclarations(manifest.memory_extensions);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (strict)
|
|
105
|
+
throw error;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function catalogForPlugins(groups, enabledOnly, candidate) {
|
|
110
|
+
const catalog = {};
|
|
111
|
+
const shadowed = new Set();
|
|
112
|
+
if (candidate !== undefined) {
|
|
113
|
+
// A candidate that removes its addendum must still hide an installed copy
|
|
114
|
+
// while that package's own documents are being validated.
|
|
115
|
+
shadowed.add(candidate.name);
|
|
116
|
+
const declarations = declarationsFromManifest(candidate, true);
|
|
117
|
+
if (declarations !== undefined)
|
|
118
|
+
catalog[candidate.name] = declarations;
|
|
119
|
+
}
|
|
120
|
+
for (const plugins of groups) {
|
|
121
|
+
for (const plugin of plugins) {
|
|
122
|
+
if (enabledOnly && !plugin.enabled)
|
|
123
|
+
continue;
|
|
124
|
+
const namespace = plugin.manifest.name;
|
|
125
|
+
if (shadowed.has(namespace) || catalog[namespace] !== undefined)
|
|
126
|
+
continue;
|
|
127
|
+
const declarations = declarationsFromManifest(plugin.manifest, false);
|
|
128
|
+
if (declarations !== undefined)
|
|
129
|
+
catalog[namespace] = declarations;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return catalog;
|
|
133
|
+
}
|
|
134
|
+
/** Declarations for a native document. Project documents use their owning
|
|
135
|
+
* project scope only, then user scope; profile/node/user docs use user scope. */
|
|
136
|
+
function pluginGroupsForDocument(doc) {
|
|
137
|
+
if (doc.scope === 'project') {
|
|
138
|
+
const marker = `${sep}.crouter${sep}`;
|
|
139
|
+
const markerIndex = doc.path.lastIndexOf(marker);
|
|
140
|
+
if (markerIndex >= 0) {
|
|
141
|
+
const root = doc.path.slice(0, markerIndex + `${sep}.crouter`.length);
|
|
142
|
+
return [listInstalledPluginsInRoot('project', root), listInstalledPlugins('user')];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return [listInstalledPlugins('user')];
|
|
146
|
+
}
|
|
147
|
+
/** Installed declarations used for validation and authoring. Disabled plugins
|
|
148
|
+
* remain present so existing values can still be checked and edited. */
|
|
149
|
+
export function memoryExtensionValidationCatalog(doc, options = {}) {
|
|
150
|
+
return catalogForPlugins(pluginGroupsForDocument(doc), false, options.candidate);
|
|
151
|
+
}
|
|
152
|
+
/** Winning enabled declarations used only to interpret structured output. */
|
|
153
|
+
export function memoryExtensionEffectiveCatalog(doc) {
|
|
154
|
+
return catalogForPlugins(pluginGroupsForDocument(doc), true);
|
|
155
|
+
}
|
|
156
|
+
/** Validate the raw `extensions` frontmatter value. Unresolved namespaces are
|
|
157
|
+
* intentionally reported rather than discarded: removal leaves raw metadata
|
|
158
|
+
* intact but lint-invalid until its plugin returns. */
|
|
159
|
+
export function validateMemoryExtensionValues(raw, catalog) {
|
|
160
|
+
if (raw === undefined)
|
|
161
|
+
return [];
|
|
162
|
+
if (!isRecord(raw))
|
|
163
|
+
return [{ path: 'extensions', message: 'expected a mapping' }];
|
|
164
|
+
const issues = [];
|
|
165
|
+
for (const [namespace, fields] of Object.entries(raw)) {
|
|
166
|
+
const namespacePath = `extensions.${namespace}`;
|
|
167
|
+
if (!isRecord(fields)) {
|
|
168
|
+
issues.push({ path: namespacePath, message: 'expected a mapping' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const declarations = catalog[namespace];
|
|
172
|
+
if (declarations === undefined) {
|
|
173
|
+
issues.push({ path: namespacePath, message: 'unresolved extension namespace' });
|
|
174
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
175
|
+
if (!isMemoryExtensionScalar(value)) {
|
|
176
|
+
issues.push({ path: `${namespacePath}.${field}`, message: 'expected a string, boolean, or finite number' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
182
|
+
const path = `${namespacePath}.${field}`;
|
|
183
|
+
if (!isMemoryExtensionScalar(value)) {
|
|
184
|
+
issues.push({ path, message: 'expected a string, boolean, or finite number' });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const declaration = declarations[field];
|
|
188
|
+
if (declaration === undefined) {
|
|
189
|
+
issues.push({ path, message: 'undeclared extension field' });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!validDefault(declaration, value)) {
|
|
193
|
+
const expected = declaration.type === 'enum' ? `one of ${declaration.values.join('|')}` : declaration.type;
|
|
194
|
+
issues.push({ path, message: `expected ${expected}` });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return issues;
|
|
199
|
+
}
|
|
200
|
+
/** Effective plugin metadata for structured consumers. It overlays only valid
|
|
201
|
+
* explicit scalars onto defaults and never mutates or serializes frontmatter. */
|
|
202
|
+
export function projectEffectiveMemoryExtensions(raw, catalog) {
|
|
203
|
+
const source = isRecord(raw) ? raw : {};
|
|
204
|
+
const effective = {};
|
|
205
|
+
for (const [namespace, declarations] of Object.entries(catalog)) {
|
|
206
|
+
const values = {};
|
|
207
|
+
const explicit = isRecord(source[namespace]) ? source[namespace] : {};
|
|
208
|
+
for (const [field, declaration] of Object.entries(declarations)) {
|
|
209
|
+
if (declaration.default !== undefined)
|
|
210
|
+
values[field] = declaration.default;
|
|
211
|
+
const value = explicit[field];
|
|
212
|
+
if (validDefault(declaration, value))
|
|
213
|
+
values[field] = value;
|
|
214
|
+
}
|
|
215
|
+
if (Object.keys(values).length > 0)
|
|
216
|
+
effective[namespace] = values;
|
|
217
|
+
}
|
|
218
|
+
return effective;
|
|
219
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const PREVIEW_RESULT_PATH_ENV = "CRTR_PREVIEW_RESULT_PATH";
|
|
2
|
+
/** Deterministic from the tool call id, so the spawning side and the result
|
|
3
|
+
* side derive the same path without sharing state. */
|
|
4
|
+
export declare function previewResultPath(toolCallId: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The private structured-result side channel between a crtr CLI invocation and
|
|
2
|
+
// the canvas broker that spawned it: the broker names one file per bash tool
|
|
3
|
+
// call, the leaf mirrors its record there, and the broker attaches the record
|
|
4
|
+
// to Pi's ToolResult.details.
|
|
5
|
+
//
|
|
6
|
+
// The path travels in the child process environment. It must never be spliced
|
|
7
|
+
// into the command text — the executed script has to stay exactly what the
|
|
8
|
+
// agent wrote, because `cmd.sh`, job inspection, and the doc substrate's
|
|
9
|
+
// `command` surfaces all read that text as the agent's own words.
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
export const PREVIEW_RESULT_PATH_ENV = 'CRTR_PREVIEW_RESULT_PATH';
|
|
13
|
+
/** A tool call id is unique only within one broker: some provider adapters
|
|
14
|
+
* synthesize a missing id from a process-local counter and a millisecond
|
|
15
|
+
* clock, so two brokers can mint the same one and collide on a shared file.
|
|
16
|
+
* Node id and pid are equal across both sides of the transport (they run in
|
|
17
|
+
* the same broker process) without either side sharing state. */
|
|
18
|
+
function brokerToken() {
|
|
19
|
+
return `${encodeURIComponent(process.env['CRTR_NODE_ID'] ?? 'no-node')}-${process.pid}`;
|
|
20
|
+
}
|
|
21
|
+
/** Deterministic from the tool call id, so the spawning side and the result
|
|
22
|
+
* side derive the same path without sharing state. */
|
|
23
|
+
export function previewResultPath(toolCallId) {
|
|
24
|
+
return join(tmpdir(), 'crouter-preview-results', `${brokerToken()}-${encodeURIComponent(toolCallId)}.json`);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Runs the synchronous configuration resolver outside crtrd's process. A
|
|
2
|
+
// network filesystem can block a native read indefinitely; unlike a worker
|
|
3
|
+
// thread, this subprocess can be killed at the launch-config timeout.
|
|
4
|
+
import { readMergedLaunchConfig } from '../config.js';
|
|
5
|
+
process.once('message', (message) => {
|
|
6
|
+
try {
|
|
7
|
+
process.send?.({ config: readMergedLaunchConfig(message.cwd, message.profileId) });
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
process.send?.({ error: error instanceof Error ? error.message : String(error) });
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type MergedLaunchConfig } from '../config.js';
|
|
1
2
|
import type { PiInvocation } from './invocation.js';
|
|
2
3
|
export { CANVAS_EXTENSIONS, CANVAS_STOPHOOK_PATH, CANVAS_INBOX_WATCHER_PATH, CANVAS_RECAP_PATH, CANVAS_GOAL_CAPTURE_PATH, CANVAS_PASSIVE_CONTEXT_PATH, CANVAS_CONTEXT_INTRO_PATH, CANVAS_TOOL_GUIDE_PATH, CANVAS_DOC_SUBSTRATE_PATH, CANVAS_STRUCTURED_OUTPUT_PATH, CANVAS_BASH_VALVE_PATH, } from './canvas-extensions.js';
|
|
3
4
|
import type { NodeMeta, LaunchSpec, Mode, Lifecycle } from '../canvas/index.js';
|
|
@@ -59,7 +60,7 @@ export declare function nextLadderModel(currentSpec: string, direction?: 1 | -1,
|
|
|
59
60
|
* a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
|
|
60
61
|
* as-given, never derived from persona frontmatter. The two canvas
|
|
61
62
|
* extensions are always first; kind-declared extensions follow. */
|
|
62
|
-
export
|
|
63
|
+
export interface LaunchBuildOptions {
|
|
63
64
|
lifecycle: Lifecycle;
|
|
64
65
|
hasManager: boolean;
|
|
65
66
|
extraEnv?: Record<string, string>;
|
|
@@ -101,10 +102,23 @@ export declare function buildLaunchSpec(kind: string, mode: Mode, opts: {
|
|
|
101
102
|
* broker's launch-time/turn-time resolution paths must bypass every
|
|
102
103
|
* fallback for and fail closed on unavailability. */
|
|
103
104
|
modelExact?: boolean;
|
|
104
|
-
}
|
|
105
|
+
}
|
|
106
|
+
/** Build a launch recipe for callers that are not on crtrd's event loop. */
|
|
107
|
+
export declare function buildLaunchSpec(kind: string, mode: Mode, opts: LaunchBuildOptions): {
|
|
105
108
|
launch: LaunchSpec;
|
|
106
109
|
lifecycle: 'terminal' | 'resident';
|
|
107
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* Resolve launch config in a bounded subprocess before building a recipe.
|
|
113
|
+
* crtrd is single-threaded: project/profile roots can live on a network
|
|
114
|
+
* filesystem, so even an ordinary synchronous config read must never run on
|
|
115
|
+
* its event loop.
|
|
116
|
+
*/
|
|
117
|
+
export declare function buildLaunchSpecAsync(kind: string, mode: Mode, opts: LaunchBuildOptions): Promise<{
|
|
118
|
+
launch: LaunchSpec;
|
|
119
|
+
lifecycle: 'terminal' | 'resident';
|
|
120
|
+
}>;
|
|
121
|
+
export declare function readMergedLaunchConfigAsync(cwd: string, profileId: string | null): Promise<MergedLaunchConfig>;
|
|
108
122
|
export type { PiInvocation } from './invocation.js';
|
|
109
123
|
/** Construct the pi invocation for a node.
|
|
110
124
|
* - fresh start: pass `prompt` (the node's first user message), no resume.
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { readMergedLaunchConfig } from '../config.js';
|
|
14
|
+
import { fork } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
14
16
|
import { LAUNCH_SYSTEM_PROMPT } from './launch-prompt.js';
|
|
15
17
|
import { nodeEnv } from './nodes.js';
|
|
16
18
|
import { editorLabel } from '../canvas/index.js';
|
|
@@ -232,27 +234,7 @@ export function nextLadderModel(currentSpec, direction = 1, providers) {
|
|
|
232
234
|
return order[0];
|
|
233
235
|
return order[(idx + direction + order.length) % order.length];
|
|
234
236
|
}
|
|
235
|
-
|
|
236
|
-
// Build the launch spec from {kind, mode}
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
/** Compose a node's full pi launch recipe from the kind registry. The system
|
|
239
|
-
* prompt is now a minimal static scaffold (`LAUNCH_SYSTEM_PROMPT`) — runtime/
|
|
240
|
-
* spine/lifecycle/kind prose is spliced per-turn from gated builtin memory docs,
|
|
241
|
-
* not baked in here. Launch KNOBS (model/tools/extensions) still come from the
|
|
242
|
-
* kind registry (`resolveKindConfig`), keyed on the FULL kind string (top-level
|
|
243
|
-
* or sub-kind). Model precedence, strongest to weakest:
|
|
244
|
-
* 1. Caller-supplied `opts.model` (durable on `meta.model_override`, re-passed
|
|
245
|
-
* on every polymorph).
|
|
246
|
-
* 2. The kind's registered model — mode-aware: orchestrator mode prefers
|
|
247
|
-
* `orchestratorModel`, falling back to `model`; base mode always uses
|
|
248
|
-
* `model`.
|
|
249
|
-
* 3. Unset → pi default.
|
|
250
|
-
* Callers pass the authoritative lifecycle + hasManager (`parent !== null`) so
|
|
251
|
-
* a polymorph/flip rebuilds the recipe faithfully; `lifecycle` is returned
|
|
252
|
-
* as-given, never derived from persona frontmatter. The two canvas
|
|
253
|
-
* extensions are always first; kind-declared extensions follow. */
|
|
254
|
-
export function buildLaunchSpec(kind, mode, opts) {
|
|
255
|
-
const merged = readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null));
|
|
237
|
+
function buildLaunchSpecFromMerged(kind, mode, opts, merged) {
|
|
256
238
|
const kindConfig = merged.kinds[kind];
|
|
257
239
|
const ladders = merged.modelLadders;
|
|
258
240
|
const kindModel = mode === 'orchestrator' ? kindConfig?.orchestratorModel ?? kindConfig?.model : kindConfig?.model;
|
|
@@ -274,6 +256,58 @@ export function buildLaunchSpec(kind, mode, opts) {
|
|
|
274
256
|
};
|
|
275
257
|
return { launch, lifecycle: opts.lifecycle };
|
|
276
258
|
}
|
|
259
|
+
/** Build a launch recipe for callers that are not on crtrd's event loop. */
|
|
260
|
+
export function buildLaunchSpec(kind, mode, opts) {
|
|
261
|
+
return buildLaunchSpecFromMerged(kind, mode, opts, readMergedLaunchConfig(opts.cwd ?? process.cwd(), opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null)));
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolve launch config in a bounded subprocess before building a recipe.
|
|
265
|
+
* crtrd is single-threaded: project/profile roots can live on a network
|
|
266
|
+
* filesystem, so even an ordinary synchronous config read must never run on
|
|
267
|
+
* its event loop.
|
|
268
|
+
*/
|
|
269
|
+
export async function buildLaunchSpecAsync(kind, mode, opts) {
|
|
270
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
271
|
+
const profileId = opts.profileId !== undefined ? opts.profileId : (process.env['CRTR_PROFILE_ID'] || null);
|
|
272
|
+
const merged = await readMergedLaunchConfigAsync(cwd, profileId);
|
|
273
|
+
return buildLaunchSpecFromMerged(kind, mode, opts, merged);
|
|
274
|
+
}
|
|
275
|
+
const LAUNCH_CONFIG_TIMEOUT_MS = 15_000;
|
|
276
|
+
export function readMergedLaunchConfigAsync(cwd, profileId) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const child = fork(fileURLToPath(new URL('./launch-config-worker.js', import.meta.url)), [], {
|
|
279
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
280
|
+
serialization: 'advanced',
|
|
281
|
+
});
|
|
282
|
+
child.send({ cwd, profileId });
|
|
283
|
+
let settled = false;
|
|
284
|
+
const finish = (fn) => {
|
|
285
|
+
if (settled)
|
|
286
|
+
return;
|
|
287
|
+
settled = true;
|
|
288
|
+
clearTimeout(timeout);
|
|
289
|
+
if (child.connected)
|
|
290
|
+
child.disconnect();
|
|
291
|
+
if (!child.killed)
|
|
292
|
+
child.kill('SIGKILL');
|
|
293
|
+
fn();
|
|
294
|
+
};
|
|
295
|
+
const timeout = setTimeout(() => finish(() => reject(new Error(`timed out after ${LAUNCH_CONFIG_TIMEOUT_MS}ms reading launch config for ${cwd}`))), LAUNCH_CONFIG_TIMEOUT_MS);
|
|
296
|
+
child.once('message', (message) => {
|
|
297
|
+
if (message.error !== undefined)
|
|
298
|
+
finish(() => reject(new Error(message.error)));
|
|
299
|
+
else if (message.config !== undefined)
|
|
300
|
+
finish(() => resolve(message.config));
|
|
301
|
+
else
|
|
302
|
+
finish(() => reject(new Error('launch config subprocess returned no result')));
|
|
303
|
+
});
|
|
304
|
+
child.once('error', (error) => finish(() => reject(error)));
|
|
305
|
+
child.once('exit', (code, signal) => {
|
|
306
|
+
if (code !== 0 && signal !== 'SIGKILL')
|
|
307
|
+
finish(() => reject(new Error(`launch config subprocess exited with code ${code}`)));
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
277
311
|
// ---------------------------------------------------------------------------
|
|
278
312
|
// Build the pi argv to launch / revive a node
|
|
279
313
|
// ---------------------------------------------------------------------------
|
|
@@ -14,13 +14,17 @@ export interface PromoteResult {
|
|
|
14
14
|
* is what a plain `crtr node yield --kind/--model` reshape rides, with no
|
|
15
15
|
* mode flip and no roadmap seeding. `promote()` below is a thin wrapper that
|
|
16
16
|
* also forces mode→orchestrator and seeds the roadmap. */
|
|
17
|
-
|
|
17
|
+
type ReshapeOptions = {
|
|
18
18
|
kind?: string;
|
|
19
19
|
model?: string;
|
|
20
20
|
mode?: Mode;
|
|
21
21
|
lifecycle?: Lifecycle;
|
|
22
22
|
profileId?: string | null;
|
|
23
|
-
}
|
|
23
|
+
};
|
|
24
|
+
/** Synchronous reshape for CLI callers outside the daemon. */
|
|
25
|
+
export declare function reshapeNode(nodeId: string, opts?: ReshapeOptions, beforePersist?: () => void): NodeMeta;
|
|
26
|
+
/** Daemon-safe reshape: network-backed config resolution happens off its event loop. */
|
|
27
|
+
export declare function reshapeNodeAsync(nodeId: string, opts?: ReshapeOptions, beforePersist?: () => void): Promise<NodeMeta>;
|
|
24
28
|
/** Promote a node to an orchestrator (mode→orchestrator), optionally
|
|
25
29
|
* specializing its kind (e.g. a `general` worker becoming a
|
|
26
30
|
* `developer.orchestrator`) and optionally also making it resident. Idempotent:
|
|
@@ -33,6 +37,12 @@ export declare function promote(nodeId: string, opts?: {
|
|
|
33
37
|
resident?: boolean;
|
|
34
38
|
model?: string;
|
|
35
39
|
}): PromoteResult;
|
|
40
|
+
/** Daemon-safe promotion variant; preserves the same validation/seed ordering. */
|
|
41
|
+
export declare function promoteAsync(nodeId: string, opts?: {
|
|
42
|
+
kind?: string;
|
|
43
|
+
resident?: boolean;
|
|
44
|
+
model?: string;
|
|
45
|
+
}): Promise<PromoteResult>;
|
|
36
46
|
export interface YieldResult {
|
|
37
47
|
meta: NodeMeta;
|
|
38
48
|
/** True iff `promote:true` was passed — a plain yield never flips mode. */
|
|
@@ -49,6 +59,13 @@ export interface YieldResult {
|
|
|
49
59
|
* same path `crtr node promote` would — mode flips to orchestrator and a
|
|
50
60
|
* roadmap scaffold is seeded. Sets intent='refresh'; the stophook shuts the
|
|
51
61
|
* process down on the next stop and the daemon revives it fresh. */
|
|
62
|
+
export declare function requestYieldAsync(nodeId: string, opts?: {
|
|
63
|
+
kind?: string;
|
|
64
|
+
model?: string;
|
|
65
|
+
promote?: boolean;
|
|
66
|
+
}, deps?: {
|
|
67
|
+
ensure?: () => void;
|
|
68
|
+
}): Promise<YieldResult>;
|
|
52
69
|
export declare function requestYield(nodeId: string, opts?: {
|
|
53
70
|
kind?: string;
|
|
54
71
|
model?: string;
|
|
@@ -56,3 +73,4 @@ export declare function requestYield(nodeId: string, opts?: {
|
|
|
56
73
|
}, deps?: {
|
|
57
74
|
ensure?: () => void;
|
|
58
75
|
}): YieldResult;
|
|
76
|
+
export {};
|