@unoverse-platform/studio 0.1.115 → 0.1.117
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/local/catalog.ts +3 -0
- package/local/plugin.ts +12 -9
- package/local/scaffold.d.mts +1 -0
- package/local/scaffold.mjs +12 -0
- package/package.json +1 -1
- package/screens/AgentChain.tsx +232 -0
- package/screens/AgentsView.tsx +85 -6
- package/screens/SkillsView.tsx +21 -3
- package/screens/TemplatesLibraryView.tsx +32 -3
- package/screens/TemplatesView.tsx +2 -2
- package/screens/index.ts +4 -1
- package/src/host.ts +5 -0
- package/src/index.css +11 -0
- package/vendor/base/src/agent-mcp/discovery.ts +5 -2
- package/vendor/base/src/agent-mcp/index.ts +3 -1
- package/vendor/base/src/agent-mcp/invoke.ts +199 -25
- package/vendor/base/src/agent-mcp/progressive.ts +124 -0
- package/vendor/base/src/agent-mcp/tools.ts +18 -11
- package/vendor/base/src/agent-mcp/types.ts +8 -0
- package/vendor/base/src/auth/agentSwitch.ts +52 -0
- package/vendor/base/src/auth/agentToken.ts +142 -0
- package/vendor/base/src/auth/auth.ts +7 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +1 -1
- package/vendor/base/src/definitions/load/expand/refs.ts +1 -1
- package/vendor/base/src/definitions/load/expand/walk.ts +1 -1
- package/vendor/base/src/definitions/load/load.ts +6 -1
- package/vendor/base/src/definitions/registry/agentFolders.ts +113 -0
- package/vendor/base/src/definitions/registry/catalog.ts +1 -1
- package/vendor/base/src/definitions/registry/objects.ts +109 -5
- package/vendor/base/src/definitions/registry/pipelines.ts +24 -5
- package/vendor/base/src/definitions/registry/taskStarts.ts +76 -0
- package/vendor/base/src/definitions/registry/taskUri.ts +67 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +39 -3
- package/vendor/base/src/items/collect.ts +10 -0
- package/vendor/base/src/items/publish.ts +10 -0
- package/vendor/base/src/lint/design/index.mjs +14 -3
- package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/src/lint/objects/index.d.mts +1 -0
- package/vendor/base/src/lint/objects/index.mjs +121 -3
- package/vendor/base/src/lint/objects/rules.d.mts +3 -0
- package/vendor/base/src/lint/objects/rules.mjs +106 -7
- package/vendor/base/src/manifests/compose.ts +3 -0
- package/vendor/base/src/manifests/executor/callback.ts +3 -2
- package/vendor/base/src/manifests/executor/memory.ts +27 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +79 -13
- package/vendor/base/src/manifests/marketplaceMcp.ts +20 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +43 -10
- package/vendor/base/src/manifests/runtime/events.ts +1 -1
- package/vendor/base/src/manifests/runtime/http/request.ts +47 -4
- package/vendor/base/src/manifests/runtime/loops/poll.ts +27 -5
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +59 -24
- package/vendor/base/src/manifests/runtime/usage.ts +13 -5
- package/vendor/base/src/platform/agentBudget.ts +65 -0
- package/vendor/base/src/platform/agentCalendar.ts +87 -0
- package/vendor/base/src/platform/inbound.ts +67 -0
- package/vendor/base/src/platform/taskChanged.ts +25 -0
- package/vendor/base/src/platform/tokenCost.ts +414 -0
- package/vendor/base/src/security/permissions.ts +10 -2
- package/vendor/sdk/appEngine.ts +42 -1
- package/vendor/sdk/lib/core/actions.ts +10 -0
- package/vendor/sdk/lib/core/apps.ts +2 -2
- package/vendor/sdk/lib/core/client.ts +11 -0
- package/vendor/sdk/lib/core/store.ts +1 -1
- package/vendor/sdk/lib/core/types.ts +2 -0
- package/vendor/sdk/lib/dom/app.ts +3 -2
- package/vendor/sdk/lib/dom/component.ts +38 -1
- package/vendor/sdk/lib/dom/element.ts +23 -0
- package/vendor/sdk/lib/dom/markdown.ts +6 -2
- package/vendor/sdk/lib/dom/primitives.ts +97 -6
- package/vendor/sdk/lib/voiceService.ts +3 -1
- package/vendor/sdk/main.ts +1 -1
- package/vendor/sdk/pageInterfaces.ts +109 -8
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AN AGENT'S TOKEN: an Agent working with nobody signed in runs as itself.
|
|
3
|
+
*
|
|
4
|
+
* Design: docs/auth-security/IDENTITY.md, Agents; docs/architecture/signal/AGENTS.md § 7. The
|
|
5
|
+
* Agent is its folder, `agent:<org>/<name>`; its token is signed by THIS CELL, not by the login
|
|
6
|
+
* provider, so it works the same whatever provider signs people in. Its permissions are its
|
|
7
|
+
* folder's `access`, capped at what its owner holds (`agentSwitch.ts`); `act` names the owner.
|
|
8
|
+
*
|
|
9
|
+
* No database here. The engine keeps the key (`engine/src/agentKeys/`) and hands it in; a
|
|
10
|
+
* test can drive every branch with a key it made itself. The one network read is
|
|
11
|
+
* `engineAgentKeys`, the public half, fetched from the engine when a token is checked.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { SignJWT, jwtVerify, decodeJwt, importJWK, generateKeyPair, exportJWK, createRemoteJWKSet, type JWK, type JWTPayload, type JWTVerifyGetKey } from "jose";
|
|
15
|
+
|
|
16
|
+
/** `iss` on every Agent token. A cell-local name: each cell signs with its own key, so a
|
|
17
|
+
* token from another cell carries the same name and still fails the signature. */
|
|
18
|
+
export const AGENT_ISSUER = "unoverse:agent";
|
|
19
|
+
|
|
20
|
+
/** Every Agent's user id starts with this, so a node can tell one from a person. */
|
|
21
|
+
export const AGENT_ID_PREFIX = "agent:";
|
|
22
|
+
|
|
23
|
+
export const AGENT_ALG = "ES256";
|
|
24
|
+
|
|
25
|
+
/** The longest a run is expected to take. A token is signed per run. */
|
|
26
|
+
export const AGENT_TOKEN_TTL_SECONDS = 3600;
|
|
27
|
+
|
|
28
|
+
export interface AgentClaims {
|
|
29
|
+
/** The Agent, `<org>/<name>`: its folder. */
|
|
30
|
+
agent: string;
|
|
31
|
+
/** Its title, for the run history. */
|
|
32
|
+
name: string;
|
|
33
|
+
/** The person who switched it on, and is accountable for it. */
|
|
34
|
+
ownerId: string;
|
|
35
|
+
/** Its `access`, already capped at the owner's own. */
|
|
36
|
+
permissions: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isAgentId(id: unknown): id is string {
|
|
40
|
+
return typeof id === "string" && id.startsWith(AGENT_ID_PREFIX);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A cell's signing key as plain JSON: what the engine stores and hands in. */
|
|
44
|
+
export interface AgentKeyPair {
|
|
45
|
+
kid: string;
|
|
46
|
+
publicJwk: JWK;
|
|
47
|
+
privateJwk: JWK;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Make a new key pair. The engine calls this once per cell and stores the result. */
|
|
51
|
+
export async function newAgentKeyPair(kid: string): Promise<AgentKeyPair> {
|
|
52
|
+
const { publicKey, privateKey } = await generateKeyPair(AGENT_ALG, { extractable: true });
|
|
53
|
+
const publicJwk = { ...(await exportJWK(publicKey)), kid, alg: AGENT_ALG, use: "sig" };
|
|
54
|
+
const privateJwk = { ...(await exportJWK(privateKey)), kid, alg: AGENT_ALG };
|
|
55
|
+
return { kid, publicJwk, privateJwk };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Sign one run's token. `audience` is the cell's `AUTH_AUDIENCE`, the same as a person's. */
|
|
59
|
+
export async function signAgentToken(
|
|
60
|
+
claims: AgentClaims,
|
|
61
|
+
key: { kid: string; privateJwk: JWK },
|
|
62
|
+
audience: string | undefined,
|
|
63
|
+
now: Date = new Date(),
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
const iat = Math.floor(now.getTime() / 1000);
|
|
66
|
+
const jwt = new SignJWT({
|
|
67
|
+
name: claims.name,
|
|
68
|
+
permissions: claims.permissions,
|
|
69
|
+
act: { sub: claims.ownerId },
|
|
70
|
+
})
|
|
71
|
+
.setProtectedHeader({ alg: AGENT_ALG, kid: key.kid, typ: "at+jwt" })
|
|
72
|
+
.setIssuer(AGENT_ISSUER)
|
|
73
|
+
.setSubject(`${AGENT_ID_PREFIX}${claims.agent}`)
|
|
74
|
+
.setIssuedAt(iat)
|
|
75
|
+
.setExpirationTime(iat + AGENT_TOKEN_TTL_SECONDS);
|
|
76
|
+
if (audience) jwt.setAudience(audience);
|
|
77
|
+
return jwt.sign(await importJWK(key.privateJwk, AGENT_ALG));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Is this token one this platform signed for an Agent? Read, not verified: it only picks
|
|
81
|
+
* which check to run. */
|
|
82
|
+
export function isAgentToken(token: string): boolean {
|
|
83
|
+
try {
|
|
84
|
+
return decodeJwt(token).iss === AGENT_ISSUER;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface VerifiedAgent {
|
|
91
|
+
id: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
ownerId?: string;
|
|
94
|
+
permissions: string[];
|
|
95
|
+
raw: JWTPayload;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check an Agent's token: signature against the cell's keys, issuer, expiry, audience.
|
|
100
|
+
* Null when any of them fails. FAIL CLOSED: a token without a `permissions` list, or whose
|
|
101
|
+
* subject is not an Agent id, is refused rather than read as "no permissions".
|
|
102
|
+
*/
|
|
103
|
+
export async function verifyAgentToken(
|
|
104
|
+
token: string,
|
|
105
|
+
keys: JWTVerifyGetKey,
|
|
106
|
+
audience: string | undefined,
|
|
107
|
+
): Promise<VerifiedAgent | null> {
|
|
108
|
+
try {
|
|
109
|
+
const { payload } = await jwtVerify(token, keys, {
|
|
110
|
+
issuer: AGENT_ISSUER,
|
|
111
|
+
algorithms: [AGENT_ALG],
|
|
112
|
+
...(audience ? { audience } : {}),
|
|
113
|
+
});
|
|
114
|
+
if (!isAgentId(payload.sub) || !Array.isArray(payload.permissions)) return null;
|
|
115
|
+
const act = payload.act as { sub?: unknown } | undefined;
|
|
116
|
+
return {
|
|
117
|
+
id: payload.sub,
|
|
118
|
+
name: typeof payload.name === "string" ? payload.name : undefined,
|
|
119
|
+
ownerId: typeof act?.sub === "string" ? act.sub : undefined,
|
|
120
|
+
permissions: (payload.permissions as unknown[]).filter((p): p is string => typeof p === "string"),
|
|
121
|
+
raw: payload,
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const code = (error as { code?: string }).code;
|
|
125
|
+
console.warn(`[auth] Agent token refused: ${code ?? (error as Error).message}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The cell's public Agent keys, from the engine's internal listener. The same address the
|
|
132
|
+
* server and the memory service already use to reach the engine (`WORKFLOW_SERVICE_URL`).
|
|
133
|
+
* jose caches the set and refetches it when a token names a key it has not seen.
|
|
134
|
+
*/
|
|
135
|
+
let engineKeys: JWTVerifyGetKey | null = null;
|
|
136
|
+
export function engineAgentKeys(): JWTVerifyGetKey {
|
|
137
|
+
if (!engineKeys) {
|
|
138
|
+
const engine = (process.env.WORKFLOW_SERVICE_URL || "http://localhost:4101").replace(/\/$/, "");
|
|
139
|
+
engineKeys = createRemoteJWKSet(new URL(`${engine}/agent-keys/jwks.json`));
|
|
140
|
+
}
|
|
141
|
+
return engineKeys;
|
|
142
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { jwtVerify, createRemoteJWKSet, JWTPayload } from "jose";
|
|
15
15
|
import { isAuthEnabled, devIdentity } from "./authConfig.js";
|
|
16
16
|
import { grantedTo } from "../security/permissions.js";
|
|
17
|
+
import { isAgentToken, verifyAgentToken, engineAgentKeys } from "./agentToken.js";
|
|
17
18
|
|
|
18
19
|
// Read env lazily (not at module load) so it's correct regardless of when .env loads.
|
|
19
20
|
//
|
|
@@ -145,6 +146,12 @@ export async function validateJWT(token: string): Promise<AuthUser | null> {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
async function verifyJWT(token: string): Promise<AuthUser | null> {
|
|
149
|
+
// AN AGENT'S TOKEN is signed by this cell, not the login provider (IDENTITY.md, Agents):
|
|
150
|
+
// checked against the cell's own keys, never the provider's. No email, no roles.
|
|
151
|
+
if (isAgentToken(token)) {
|
|
152
|
+
const agent = await verifyAgentToken(token, engineAgentKeys(), getAudience());
|
|
153
|
+
return agent ? { id: agent.id, roles: [], permissions: agent.permissions, raw: agent.raw } : null;
|
|
154
|
+
}
|
|
148
155
|
const jwksSet = await getJWKS();
|
|
149
156
|
const issuers = getIssuerCandidates();
|
|
150
157
|
if (!jwksSet || issuers.length === 0) {
|
|
@@ -112,7 +112,7 @@ export function inheritProps(
|
|
|
112
112
|
// could send, which is exactly the venue-renaming this flag exists to prevent.
|
|
113
113
|
// `input` TRAVELS TOO (2026-09-15). An atom that marks a field `input: false` (the button's
|
|
114
114
|
// `icon` and `link`) says nobody outside fills it; without the word here the host inherited
|
|
115
|
-
// the field as fillable and the tool asked the model for it (
|
|
115
|
+
// the field as fillable and the tool asked the model for it (a bank's transfer form: four fields it
|
|
116
116
|
// could not honestly write). Same failure as `override` above, one word later.
|
|
117
117
|
const CONTRACT = ["type", "description", "maxLength", "minItems", "maxItems", "items", "hydrate", "optional", "override", "preview", "input"] as const;
|
|
118
118
|
// AN ATOM INSIDE AN `Each` DESCRIBES THE ITEM, not the page. The scope is the chain of
|
|
@@ -83,7 +83,7 @@ function applyStyleLiterals(style: unknown, lits: Record<string, unknown>): void
|
|
|
83
83
|
|
|
84
84
|
export function applyLiterals(node: AnyNode, lits: Record<string, unknown>): void {
|
|
85
85
|
// A NESTED REF FORWARDS THE LITERAL. A piece whose kind is itself a Ref to an atom
|
|
86
|
-
// (
|
|
86
|
+
// (a bank's button: primary = the marketplace `button` atom with `props: {label: label}`)
|
|
87
87
|
// hands its host's literal on as that Ref's `with`, so the atom draws the words whether
|
|
88
88
|
// the tree is expanded here or resolved by name on the client (referenced form).
|
|
89
89
|
if (node.type === "Ref" && node.props && typeof node.props === "object") {
|
|
@@ -61,7 +61,7 @@ export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<
|
|
|
61
61
|
* silently resolved to the marketplace `button` atom and the org's own button component
|
|
62
62
|
* was never reached (2026-09-07). A bare name keeps the old order: atom first.
|
|
63
63
|
*
|
|
64
|
-
* AN EMBEDDED PIECE'S PRIVATE AXIS IS CHOSEN WHERE IT IS COMPOSED (2026-09-07, the
|
|
64
|
+
* AN EMBEDDED PIECE'S PRIVATE AXIS IS CHOSEN WHERE IT IS COMPOSED (2026-09-07, the a bank
|
|
65
65
|
* button: kinds primary/secondary/outline/link on a private `kind` axis). Its root is the
|
|
66
66
|
* compiled surface shell around the switches on that axis. Inlined, the piece has no slice
|
|
67
67
|
* of its own, so the host's `with` names the kind, and a host that names none gets the
|
|
@@ -184,7 +184,12 @@ export function loadDefinition(
|
|
|
184
184
|
if (m.preview && typeof m.preview === "object") def.preview = m.preview as Record<string, string[]>;
|
|
185
185
|
// THE WORKFLOW THAT WORKS IT (UNOVERSE_HOST_PAGE.md rule 4): the manifest names it,
|
|
186
186
|
// and the page reads it here to start a call on it.
|
|
187
|
-
if (m.binding && typeof m.binding === "object") def.binding = m.binding as { workflow?: string; trigger?: string; pipeline?: string };
|
|
187
|
+
if (m.binding && typeof m.binding === "object") def.binding = m.binding as { workflow?: string; trigger?: string; pipeline?: string; autoTrigger?: boolean };
|
|
188
|
+
// THE TASK IT CARRIES (docs/design/lifecycle-hooks.md): which moments have a hook, so a
|
|
189
|
+
// page that draws it knows to ask for its task. Only the moments ride; the calls stay
|
|
190
|
+
// on the server, in the template's folder.
|
|
191
|
+
if (Array.isArray(m.lifecycle))
|
|
192
|
+
(def as unknown as Record<string, unknown>).lifecycle = (m.lifecycle as Array<Record<string, unknown>>).map((e) => ({ phase: e?.phase }));
|
|
188
193
|
// SPATIAL-CAPABLE, exactly as a component is (§9c, ruled 2026-08-30): a
|
|
189
194
|
// template is found by what a guest says, through the same projection, the
|
|
190
195
|
// same registry switch and the same embedded text. The difference is only
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENTS AS FOLDERS, READ (docs/architecture/signal/AGENTS.md § 3, § 3a).
|
|
3
|
+
*
|
|
4
|
+
* One shape for every Agent: `agents/<name>/agent.yaml` and its soul (`SOUL.md`). This reads them
|
|
5
|
+
* for whoever shows or runs an Agent (Studio's Agents lane, Signal, Spatial). A flat file directly
|
|
6
|
+
* in `agents/` is the older Agent file (agents.ts), read there until it is converted.
|
|
7
|
+
*
|
|
8
|
+
* Read as written: the lint (`lintObjects`) is what refuses a broken one; here a folder that cannot
|
|
9
|
+
* be read is left out with a warning, never taking the org's other Agents with it
|
|
10
|
+
* (UNOVERSE_ORGANISATION.md § 11).
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { parse } from "yaml";
|
|
15
|
+
import { listOrgs, projectDir } from "./homes.js";
|
|
16
|
+
import { isolated } from "../isolated.js";
|
|
17
|
+
|
|
18
|
+
/** One Agent trigger, as the file writes it: what wakes it, and the node behind it. */
|
|
19
|
+
export interface AgentTrigger {
|
|
20
|
+
name: string;
|
|
21
|
+
node?: string;
|
|
22
|
+
wakes?: string;
|
|
23
|
+
config?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** An Agent folder: its file as written, its soul's words, where it is. */
|
|
27
|
+
export interface AgentFolder {
|
|
28
|
+
/** `<org>/<name>`. */
|
|
29
|
+
ref: string;
|
|
30
|
+
org: string;
|
|
31
|
+
name: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
role?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
whenToUse?: string;
|
|
36
|
+
category?: string;
|
|
37
|
+
version?: string;
|
|
38
|
+
avatar?: string;
|
|
39
|
+
skills: string[];
|
|
40
|
+
model?: { use?: string; reasoning?: string; fallback?: string; turns?: number };
|
|
41
|
+
budget?: { perDay?: string; perRun?: string };
|
|
42
|
+
tasks: string[];
|
|
43
|
+
access: string[];
|
|
44
|
+
triggers: AgentTrigger[];
|
|
45
|
+
checkIn?: string;
|
|
46
|
+
memory?: { agent?: string };
|
|
47
|
+
askWhenUnsure?: string;
|
|
48
|
+
/** The soul's own words (character only), empty when the file is missing. */
|
|
49
|
+
soul: string;
|
|
50
|
+
/** The folder, relative to the org repo. */
|
|
51
|
+
path: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const str = (v: unknown): string | undefined => (typeof v === "string" && v.trim() ? v.trim() : undefined);
|
|
55
|
+
const strs = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string" && !!x.trim()) : []);
|
|
56
|
+
const rec = (v: unknown): Record<string, unknown> | undefined => (v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined);
|
|
57
|
+
|
|
58
|
+
/** One Agent folder of an org, or null when it is not one. */
|
|
59
|
+
export function readAgentFolder(root: string, org: string, name: string): AgentFolder | null {
|
|
60
|
+
const dir = join(root, "agents", name);
|
|
61
|
+
const file = join(dir, "agent.yaml");
|
|
62
|
+
if (!existsSync(file)) return null;
|
|
63
|
+
const doc = (parse(readFileSync(file, "utf8")) as Record<string, unknown>) ?? {};
|
|
64
|
+
if (doc.type !== "agent") return null;
|
|
65
|
+
const soulName = str(doc.soul) ?? "SOUL.md";
|
|
66
|
+
const soulFile = join(dir, soulName);
|
|
67
|
+
const model = rec(doc.model);
|
|
68
|
+
const budget = rec(doc.budget);
|
|
69
|
+
const memory = rec(doc.memory);
|
|
70
|
+
return {
|
|
71
|
+
ref: `${org}/${name}`,
|
|
72
|
+
org,
|
|
73
|
+
name,
|
|
74
|
+
title: str(doc.title),
|
|
75
|
+
role: str(doc.role),
|
|
76
|
+
description: str(doc.description),
|
|
77
|
+
whenToUse: str(doc.whenToUse),
|
|
78
|
+
category: str(doc.category),
|
|
79
|
+
version: str(doc.version),
|
|
80
|
+
avatar: str(doc.avatar),
|
|
81
|
+
skills: strs(doc.skills),
|
|
82
|
+
...(model ? { model: { use: str(model.use), reasoning: str(model.reasoning), fallback: str(model.fallback), ...(typeof model.turns === "number" && model.turns > 0 ? { turns: Math.floor(model.turns) } : {}) } } : {}),
|
|
83
|
+
...(budget ? { budget: { perDay: str(budget.perDay), perRun: str(budget.perRun) } } : {}),
|
|
84
|
+
tasks: strs(doc.tasks),
|
|
85
|
+
access: strs(doc.access),
|
|
86
|
+
triggers: (Array.isArray(doc.triggers) ? doc.triggers : [])
|
|
87
|
+
.map(rec)
|
|
88
|
+
.filter((c): c is Record<string, unknown> => !!c && !!str(c.name))
|
|
89
|
+
.map((c) => ({ name: str(c.name)!, node: str(c.node), wakes: str(c.wakes), ...(rec(c.config) ? { config: rec(c.config) } : {}) })),
|
|
90
|
+
checkIn: str(doc.checkIn),
|
|
91
|
+
...(memory ? { memory: { agent: str(memory.agent) } } : {}),
|
|
92
|
+
askWhenUnsure: str(doc.askWhenUnsure),
|
|
93
|
+
soul: existsSync(soulFile) ? readFileSync(soulFile, "utf8") : "",
|
|
94
|
+
path: `agents/${name}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every Agent folder of one org, by name. */
|
|
99
|
+
export function listAgentFoldersIn(root: string, org: string): AgentFolder[] {
|
|
100
|
+
const dir = join(root, "agents");
|
|
101
|
+
if (!existsSync(dir)) return [];
|
|
102
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
103
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_"))
|
|
104
|
+
.map((e) => isolated(`agent ${org}/${e.name}`, () => readAgentFolder(root, org, e.name)) ?? null)
|
|
105
|
+
.filter((a): a is AgentFolder => a !== null)
|
|
106
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every Agent folder across the workspace, or one org's. */
|
|
110
|
+
export function listAgentFolders(org?: string): AgentFolder[] {
|
|
111
|
+
const orgs = org && org !== "all" ? [org] : listOrgs();
|
|
112
|
+
return orgs.flatMap((o) => listAgentFoldersIn(projectDir(o), o));
|
|
113
|
+
}
|
|
@@ -290,7 +290,7 @@ export function loadAppManifest(ref: string): AppManifest | null {
|
|
|
290
290
|
// the template loader); the app loader never did, so `app.previewComponents?.[0]`
|
|
291
291
|
// was undefined and the app tool's whole render block — gated on that ref — was
|
|
292
292
|
// skipped. The call returned cleanly, nothing reached /stream, and the model told
|
|
293
|
-
// the person to use a surface that had never appeared (
|
|
293
|
+
// the person to use a surface that had never appeared (a bank's voice app, 2026-09-07).
|
|
294
294
|
// `kind` rides along for the same reason it does on a template: the render path
|
|
295
295
|
// resolves the definition in the tier `kind` names, and an app is not a component.
|
|
296
296
|
return normalizeMode({
|
|
@@ -94,6 +94,8 @@ export interface ObjectLink {
|
|
|
94
94
|
match?: Record<string, string>;
|
|
95
95
|
/** Fields (of `between`, or the row's side of `match`) whose values may name one thing several ways: one question per set of names says which are the same. */
|
|
96
96
|
alike?: string[];
|
|
97
|
+
/** Fields of the row this line carries, added never replaced: a provider's `serves` carries its role (relationships-and-labels.md). */
|
|
98
|
+
carries?: string[];
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
@@ -112,10 +114,26 @@ export interface ObjectMatching {
|
|
|
112
114
|
evidence?: string[];
|
|
113
115
|
/** Filled on both and different, as words: never the same thing. */
|
|
114
116
|
ruleOut?: string[];
|
|
115
|
-
/** The node that decides a name the rules leave open (TypeSafeChoice)
|
|
116
|
-
|
|
117
|
+
/** The node that decides a name the rules leave open: a node's name (TypeSafeChoice), or a decision node with its
|
|
118
|
+
* config ({ node: OpenAIDecision, config: { model } }), asked as the review is. Absent, an open name goes to review. */
|
|
119
|
+
judge?: string | { node: string; config?: Record<string, unknown> };
|
|
120
|
+
/**
|
|
121
|
+
* THE LINKER, BEFORE THE JUDGE (pipelines README, "Three sweeps"; owner 2026-10-10: "Splink to dedupe
|
|
122
|
+
* our entities"): a node that scores the name against each candidate's names. Sure the same, it links;
|
|
123
|
+
* sure different, the name is none of them; between, the judge decides.
|
|
124
|
+
*/
|
|
125
|
+
link?: { node: string; config: Record<string, unknown> };
|
|
126
|
+
/**
|
|
127
|
+
* THE LAST LOOK (owner 2026-10-11: "use the LLM to investigate and solve these"): the node that
|
|
128
|
+
* looks again at every name a run left open, with what the documents say where it is used
|
|
129
|
+
* (OpenAIDecision). Sure, it decides as a person would, with Undo; not sure, it stays open.
|
|
130
|
+
* Absent, the judge looks.
|
|
131
|
+
*/
|
|
132
|
+
review?: { node: string; config?: Record<string, unknown> };
|
|
117
133
|
/** The lines (by name, e.g. a document's `about`) whose name, sure to be no record held, makes the record. A mention on any other line never does. */
|
|
118
134
|
createFrom?: string[];
|
|
135
|
+
/** The object's `extract.never` words, set by the registry: left out when two names of one parent are compared ("A Shares" and "Class A"). */
|
|
136
|
+
never?: string[];
|
|
119
137
|
}
|
|
120
138
|
|
|
121
139
|
/**
|
|
@@ -130,6 +148,18 @@ export interface DatasetColumn {
|
|
|
130
148
|
* its words here and the database refuses the rest: a misspelling cannot become a second
|
|
131
149
|
* thing that never sums with the first. Absent on a column carrying a document's own words. */
|
|
132
150
|
enum?: string[];
|
|
151
|
+
/** FOR A COLUMN WITH `enum`: the words a table's title or headings print for each value (net return:
|
|
152
|
+
* [performance, returns]), so a table found by its shape is known by what it holds (tables.md 3b). */
|
|
153
|
+
wordings?: Record<string, string[]>;
|
|
154
|
+
/** FOR A COLUMN WITH `enum`: the words a key-figures row's own label prints for each value (net asset
|
|
155
|
+
* value: [total net assets, fund size]), so "Total Net Assets | USD 126m" is one figure (tables.md 3b). */
|
|
156
|
+
rowWordings?: Record<string, string[]>;
|
|
157
|
+
/** FOR A COLUMN WITH `enum`: the words a table's COLUMN prints for each value (exposure: [market value, % of net
|
|
158
|
+
* assets]), so a number kept from any table is known by the column it sits under (tables.md 3f, 2026-10-16). */
|
|
159
|
+
columnWordings?: Record<string, string[]>;
|
|
160
|
+
/** FOR A COLUMN WITH `enum`: the values of another column each of its values gathers (dimension fair value level:
|
|
161
|
+
* [Level 1, Level 2, Level 3]), so a number whose row or column names one is known to be sliced that way (tables.md 3i). */
|
|
162
|
+
values?: Record<string, string[]>;
|
|
133
163
|
/** string | number | integer | date | datetime | boolean. Nothing else: a type this does not
|
|
134
164
|
* name is one the generated table cannot promise anything about. */
|
|
135
165
|
type: string;
|
|
@@ -152,6 +182,35 @@ export interface DatasetDefinition {
|
|
|
152
182
|
subject?: string;
|
|
153
183
|
asOf?: string;
|
|
154
184
|
description?: string;
|
|
185
|
+
/** WHOSE A ROW IS, AS RECORDS (tables.md 3i step 4, 2026-10-17: the code names no industry): the object a row's subject is,
|
|
186
|
+
* its parts, how one sits inside another, and the `level` values each is filed under. Absent: rows name no record. */
|
|
187
|
+
whose?: DatasetWhose;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface DatasetWhose {
|
|
191
|
+
/** The object a row's subject is (a fund), the column its name goes in, the `level` value for its own figures. */
|
|
192
|
+
object: string; column: string; level: string;
|
|
193
|
+
/** The subject's field of other names, the field naming what it is compared with (its index), and the `level` value for that. */
|
|
194
|
+
names?: string; compare?: string; compareLevel?: string;
|
|
195
|
+
/** How a subject sits inside another: the link from it to its parent, and the field naming the parent. */
|
|
196
|
+
within?: { link: string; field?: string };
|
|
197
|
+
/** Its parts (a fund's share classes): their object, their column, their `level` value, their name field, and the field naming their subject. */
|
|
198
|
+
part?: { object: string; column: string; level: string; name: string; of: string; measures?: string[]; perUnit?: Record<string, string> };
|
|
199
|
+
/** The subject's own measures: a figure of one is the subject's, over a part only its section named. */
|
|
200
|
+
measures?: string[];
|
|
201
|
+
/** The measures that are a record's standing terms (a fee rate): a figure of one is a field, never a row. */
|
|
202
|
+
terms?: string[];
|
|
203
|
+
/** The measures that are a subject's figure against its comparison (an excess return): a row of one that names no
|
|
204
|
+
* subject is the subject the table's own rows name, when they name exactly one. */
|
|
205
|
+
relative?: string[];
|
|
206
|
+
/** The words a name of the subject's kind carries (fund, portfolio): an allocation's row naming one, with words of its
|
|
207
|
+
* own, is a holding (tables.md 3c rule 3). Absent, the word fund. */
|
|
208
|
+
vehicles?: string[];
|
|
209
|
+
/** Who stands behind the subject (a fund's manager): its object, as a layout's section names it in `about`, and the
|
|
210
|
+
* `level` value for its figures (the firm's assets under management), reached from a row's subject by a join. */
|
|
211
|
+
owner?: { object: string; level: string };
|
|
212
|
+
/** The `level` value for a document about no subject (a market note). */
|
|
213
|
+
none?: string;
|
|
155
214
|
}
|
|
156
215
|
|
|
157
216
|
export interface ObjectFunction {
|
|
@@ -206,6 +265,8 @@ export interface ObjectDefinition {
|
|
|
206
265
|
links?: Record<string, ObjectLink>;
|
|
207
266
|
/** How a name is matched to its rows (entity-resolution.md); absent, a line to it compares one field with one field. */
|
|
208
267
|
matching?: ObjectMatching;
|
|
268
|
+
/** How a page reader picks it out of a page (pipelines README, "Or read by a node"): instructions and worked examples. */
|
|
269
|
+
extract?: { instructions?: string; examples?: { text: string; answer: Record<string, unknown>[] }[]; never?: string[]; notAlso?: string[]; notAlsoWhen?: Record<string, Record<string, string[]>>; notWhen?: string[] };
|
|
209
270
|
/** THE TABLE THIS OBJECT OWNS (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18),
|
|
210
271
|
* from its folder's table.yaml: an object that holds many small facts read together rather
|
|
211
272
|
* than one row read alone. Absent on every ordinary object, which stays a row. */
|
|
@@ -275,12 +336,54 @@ export function readDataset(name: string, file: Record<string, any>, dir: string
|
|
|
275
336
|
// about the column.
|
|
276
337
|
if (allowed && type !== "string") findings.push(`column "${col}": only a string column may say which values it allows`);
|
|
277
338
|
if (allowed && new Set(allowed).size !== allowed.length) findings.push(`column "${col}": its allowed values repeat`);
|
|
278
|
-
|
|
339
|
+
// ITS WORDS (tables.md 3b): what a table prints for each allowed value; a key outside the list is no word of it.
|
|
340
|
+
const wordings = c?.wordings && typeof c.wordings === "object" ? Object.fromEntries(Object.entries(c.wordings as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
341
|
+
if (wordings && !allowed) findings.push(`column "${col}": wordings name the words for allowed values, so the column says enum`);
|
|
342
|
+
for (const k of Object.keys(wordings ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": wordings for "${k}", which is not one of its allowed values`);
|
|
343
|
+
// AND A ROW'S OWN WORDS (key figures, tables.md 3b): kept like `wordings`, or the reader never sees them
|
|
344
|
+
// (measured 2026-10-14: dropped here, no key figure was read).
|
|
345
|
+
const rowWordings = c?.rowWordings && typeof c.rowWordings === "object" ? Object.fromEntries(Object.entries(c.rowWordings as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
346
|
+
if (rowWordings && !allowed) findings.push(`column "${col}": rowWordings name the words for allowed values, so the column says enum`);
|
|
347
|
+
for (const k of Object.keys(rowWordings ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": rowWordings for "${k}", which is not one of its allowed values`);
|
|
348
|
+
// AND A COLUMN'S OWN WORDS (every number kept, tables.md 3f): kept like `wordings`.
|
|
349
|
+
const columnWordings = c?.columnWordings && typeof c.columnWordings === "object" ? Object.fromEntries(Object.entries(c.columnWordings as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
350
|
+
if (columnWordings && !allowed) findings.push(`column "${col}": columnWordings name the words for allowed values, so the column says enum`);
|
|
351
|
+
for (const k of Object.keys(columnWordings ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": columnWordings for "${k}", which is not one of its allowed values`);
|
|
352
|
+
// AND THE VALUES EACH OF ITS VALUES GATHERS (a slicing's own values, tables.md 3i): kept like `wordings`.
|
|
353
|
+
const values = c?.values && typeof c.values === "object" ? Object.fromEntries(Object.entries(c.values as Record<string, unknown>).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : []])) : undefined;
|
|
354
|
+
if (values && !allowed) findings.push(`column "${col}": values name what allowed values gather, so the column says enum`);
|
|
355
|
+
for (const k of Object.keys(values ?? {})) if (allowed && !allowed.includes(k)) findings.push(`column "${col}": values for "${k}", which is not one of its allowed values`);
|
|
356
|
+
columns.push({ name: col, type, ...(allowed ? { enum: allowed } : {}), ...(wordings ? { wordings } : {}), ...(rowWordings ? { rowWordings } : {}), ...(columnWordings ? { columnWordings } : {}), ...(values ? { values } : {}), ...(c?.required === true ? { required: true } : {}), ...(c?.indexed === true ? { indexed: true } : {}), ...(c?.description ? { description: String(c.description) } : {}) });
|
|
279
357
|
}
|
|
280
358
|
const uniqueBy = Array.isArray(file.uniqueBy) ? file.uniqueBy.map((u: unknown) => String(u)) : undefined;
|
|
281
359
|
for (const u of uniqueBy ?? []) if (!seen.has(u) && !RESERVED.has(u)) findings.push(`uniqueBy names "${u}", which is not a column`);
|
|
282
360
|
if (findings.length) throw new Error(`object ${name}: ${findings.join("; ")} (${dir})`);
|
|
283
|
-
|
|
361
|
+
// WHOSE A ROW IS, AS RECORDS: kept as declared, its columns checked.
|
|
362
|
+
const w = file.whose && typeof file.whose === "object" ? (file.whose as Record<string, any>) : null;
|
|
363
|
+
const whose: DatasetWhose | undefined = w && w.object && w.column && w.level ? {
|
|
364
|
+
object: String(w.object), column: String(w.column), level: String(w.level),
|
|
365
|
+
...(w.names ? { names: String(w.names) } : {}), ...(w.compare ? { compare: String(w.compare) } : {}), ...(w.compareLevel ? { compareLevel: String(w.compareLevel) } : {}),
|
|
366
|
+
...(w.within?.link ? { within: { link: String(w.within.link), ...(w.within.field ? { field: String(w.within.field) } : {}) } } : {}),
|
|
367
|
+
// A PART'S OWN MEASURES (2026-10-24): a figure of one of them is always a part's ("net asset value per share" is a
|
|
368
|
+
// share class's; a fund has no price per share of its own), whatever section it is printed in.
|
|
369
|
+
...(w.part?.object ? { part: { object: String(w.part.object), column: String(w.part.column), level: String(w.part.level), name: String(w.part.name), of: String(w.part.of), ...(Array.isArray(w.part.measures) ? { measures: w.part.measures.map(String) } : {}), ...(w.part.perUnit && typeof w.part.perUnit === "object" ? { perUnit: Object.fromEntries(Object.entries(w.part.perUnit).map(([k, v]) => [k, String(v)])) } : {}) } } : {}),
|
|
370
|
+
...(w.none ? { none: String(w.none) } : {}),
|
|
371
|
+
...(Array.isArray(w.measures) ? { measures: w.measures.map(String) } : {}),
|
|
372
|
+
// THE MEASURES THAT ARE TERMS (2026-10-25): a figure of one is a field of its record, never a row ("static goes into
|
|
373
|
+
// the entities, changes over time goes into the metrics").
|
|
374
|
+
...(Array.isArray(w.terms) ? { terms: w.terms.map(String) } : {}),
|
|
375
|
+
// THE MEASURES THAT ARE A FIGURE AGAINST A COMPARISON (2026-10-25): "Excess" printed under a class's and its index's
|
|
376
|
+
// rows is that class's return above the index, as the measure's own definition says.
|
|
377
|
+
...(Array.isArray(w.relative) ? { relative: w.relative.map(String) } : {}),
|
|
378
|
+
// WHAT A HOLDING OF THE SUBJECT'S KIND IS CALLED IN ITS NAME (tables.md 3c rule 3, 2026-10-26): a fund, a segregated portfolio.
|
|
379
|
+
...(Array.isArray(w.vehicles) ? { vehicles: w.vehicles.map(String) } : {}),
|
|
380
|
+
// WHO STANDS BEHIND THE SUBJECT (2026-10-25): a section about a fund's manager prints the firm's figures, not the fund's.
|
|
381
|
+
...(w.owner?.object && w.owner?.level ? { owner: { object: String(w.owner.object), level: String(w.owner.level) } } : {}),
|
|
382
|
+
} : undefined;
|
|
383
|
+
if (w && !whose) findings.push("whose names its object, its column and its level");
|
|
384
|
+
for (const col of [whose?.column, whose?.part?.column].filter(Boolean) as string[]) if (!seen.has(col)) findings.push(`whose names column "${col}", which is not a column`);
|
|
385
|
+
if (findings.length) throw new Error(`object ${name}: ${findings.join("; ")} (${dir})`);
|
|
386
|
+
return { columns, ...(uniqueBy ? { uniqueBy } : {}), ...(file.description ? { description: String(file.description) } : {}), ...(whose ? { whose } : {}) };
|
|
284
387
|
}
|
|
285
388
|
|
|
286
389
|
function readFolder(dir: string, name: string, org: string | undefined, seen: string[]): ObjectDefinition {
|
|
@@ -367,7 +470,8 @@ function readFolder(dir: string, name: string, org: string | undefined, seen: st
|
|
|
367
470
|
...(typeof envelope.element === "string" && envelope.element ? { element: envelope.element } : parent?.element ? { element: parent.element } : {}),
|
|
368
471
|
...(envelope.meta ? { meta: envelope.meta } : parent?.meta ? { meta: parent.meta } : {}),
|
|
369
472
|
...(links && Object.keys(links).length ? { links } : {}),
|
|
370
|
-
...(envelope.matching ? { matching: envelope.matching as ObjectMatching } : parent?.matching ? { matching: parent.matching } : {}),
|
|
473
|
+
...(envelope.matching ? { matching: { ...(envelope.matching as ObjectMatching), ...((envelope.extract as ObjectDefinition["extract"])?.never?.length ? { never: (envelope.extract as ObjectDefinition["extract"])!.never } : {}) } } : parent?.matching ? { matching: parent.matching } : {}),
|
|
474
|
+
...(envelope.extract ? { extract: envelope.extract as ObjectDefinition["extract"] } : {}),
|
|
371
475
|
...(dataset ? { dataset } : {}),
|
|
372
476
|
props,
|
|
373
477
|
ownProps,
|
|
@@ -68,11 +68,28 @@ export interface MakeEntry {
|
|
|
68
68
|
/** How many of this object one source makes (owner, 2026-09-13): one, or many. The read asks by it; a part's entry says none. */
|
|
69
69
|
cardinality?: "one" | "many";
|
|
70
70
|
/**
|
|
71
|
-
* WRITTEN FROM EVERY
|
|
72
|
-
* document
|
|
73
|
-
* document's first read, which sees only its first two pages
|
|
71
|
+
* WRITTEN FROM EVERY PAGE (owner, 2026-10-05; page-store.md, 2026-10-07: no sections): `per: page`
|
|
72
|
+
* writes this object once the document's pages are kept, one read per page, instead of in the
|
|
73
|
+
* document's first read, which sees only its first two pages.
|
|
74
74
|
*/
|
|
75
|
-
per?: "
|
|
75
|
+
per?: "page";
|
|
76
|
+
/**
|
|
77
|
+
* READ BY A NODE, FROM THE PAGES THAT NAME IT (pipelines README, "Or read by a node", owner
|
|
78
|
+
* 2026-10-09): with `per: page`, the node handed the best chunks with a template made from the
|
|
79
|
+
* object's input props, in place of a model reading every page. Its config is resolved over the
|
|
80
|
+
* run's `config` when the rows are collected.
|
|
81
|
+
*/
|
|
82
|
+
read?: { node: string; config: Record<string, unknown>; chunks?: number | "all"; cacheable?: true };
|
|
83
|
+
/**
|
|
84
|
+
* READ FROM THE DOCUMENT'S TABLES TOO (pipelines/tables.md, 2026-10-12): a table's labels are
|
|
85
|
+
* matched to the object's props by their `wordings`, and a matched cell's value is copied as
|
|
86
|
+
* printed. `whose` is the node that says which of the document's rows a table is about, when
|
|
87
|
+
* the document is linked to more than one. `labels` is the node that proposes a field for a label
|
|
88
|
+
* no rule matched (column matching step 4), once per org, for a person to approve in Review.
|
|
89
|
+
*/
|
|
90
|
+
/** `allocations` names the object's line an allocation table draws, one per fund it holds, carrying the weight (tables.md 3c). */
|
|
91
|
+
/** `boards` names the object's lines a board list may draw, each to whose board it is, carrying the role (tables.md 3d). */
|
|
92
|
+
tables?: { whose?: { node: string; config: Record<string, unknown> }; subject?: boolean; labels?: { node: string; config: Record<string, unknown> }; allocations?: string; boards?: string[] };
|
|
76
93
|
sourceUrl?: string;
|
|
77
94
|
sourceId?: string;
|
|
78
95
|
/** The item's content, each value a reference over `item`, `index`, `config`, `collect`. */
|
|
@@ -154,7 +171,9 @@ function readFolder(dir: string, name: string, org: string | undefined): Pipelin
|
|
|
154
171
|
...(spec.any === true ? { any: list } : { each: list }),
|
|
155
172
|
...(spec.cardinality === "one" || spec.cardinality === "many" ? { cardinality: spec.cardinality as "one" | "many" } : {}),
|
|
156
173
|
...(whenOf(spec) ? { when: whenOf(spec) } : {}),
|
|
157
|
-
...(spec.per === "
|
|
174
|
+
...(spec.per === "page" ? { per: "page" as const } : {}),
|
|
175
|
+
...(isRecord(spec.read) && typeof spec.read.node === "string" ? { read: { node: spec.read.node, config: isRecord(spec.read.config) ? spec.read.config : {}, ...(spec.read.chunks === "all" || typeof spec.read.chunks === "number" ? { chunks: spec.read.chunks as number | "all" } : {}), ...(spec.read.cacheable === true ? { cacheable: true as const } : {}) } } : {}),
|
|
176
|
+
...(isRecord(spec.tables) ? { tables: { ...(isRecord(spec.tables.whose) && typeof spec.tables.whose.node === "string" ? { whose: { node: spec.tables.whose.node, config: isRecord(spec.tables.whose.config) ? spec.tables.whose.config : {} } } : {}), ...(spec.tables.subject === true ? { subject: true } : {}), ...(isRecord(spec.tables.labels) && typeof spec.tables.labels.node === "string" ? { labels: { node: spec.tables.labels.node, config: isRecord(spec.tables.labels.config) ? spec.tables.labels.config : {} } } : {}), ...(typeof spec.tables.allocations === "string" && spec.tables.allocations ? { allocations: spec.tables.allocations } : {}), ...(Array.isArray(spec.tables.boards) && spec.tables.boards.length && spec.tables.boards.every((b: unknown) => typeof b === "string" && b) ? { boards: spec.tables.boards as string[] } : {}) } } : {}),
|
|
158
177
|
sourceUrl: typeof raw.sourceUrl === "string" ? raw.sourceUrl : "{{ item.address }}",
|
|
159
178
|
sourceId: typeof raw.sourceId === "string" ? raw.sourceId : "{{ item.universalId }}",
|
|
160
179
|
content: isRecord(raw.content) ? raw.content : {},
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A TASK START IS A TASK (docs/mcp/MCP_TASKS.md § 2): a titled, described Task Start has an MCP
|
|
3
|
+
* tool on our own door, so any Agent a line is drawn to on its canvas can call it, and is
|
|
4
|
+
* answered with an MCP task. One rule, read by the engine's Task Start (what it offers a wired
|
|
5
|
+
* Agent) and the server (the tool it registers), so the two always name the same tool.
|
|
6
|
+
*/
|
|
7
|
+
import { AGENT_CARD, listAgents, type AgentFile } from "./agents.js";
|
|
8
|
+
import { listApps } from "./catalog.js";
|
|
9
|
+
|
|
10
|
+
/** A Task Start as a workflow saves it. */
|
|
11
|
+
export interface TaskStartNode {
|
|
12
|
+
id: string;
|
|
13
|
+
type?: string;
|
|
14
|
+
data?: { config?: Record<string, unknown> };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const kebab = (s: string) =>
|
|
18
|
+
s
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
21
|
+
.replace(/[^a-zA-Z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-+|-+$/g, "")
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
|
|
25
|
+
const text = (v: unknown) => (typeof v === "string" ? v.trim() : "");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A TASK START'S NAME (docs/mcp/MCP_TASKS.md § 2a, step U2): its own `name` field, set once, so its
|
|
29
|
+
* URI (`unoverse://tasks/<org>/<name>`) never moves when someone renames the title. A Task Start
|
|
30
|
+
* saved before the field existed is named from its title, as it always was, and that name is
|
|
31
|
+
* written into the field on its next save (`withFixedTaskName`), so from then on it stays put.
|
|
32
|
+
*/
|
|
33
|
+
export function taskStartName(node: TaskStartNode): string {
|
|
34
|
+
const cfg = node.data?.config ?? {};
|
|
35
|
+
return text(cfg.name) || kebab(text(cfg.title));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The node with its name written in, when it has a title and no name yet. Saving calls this. */
|
|
39
|
+
export function withFixedTaskName<T extends TaskStartNode>(node: T): T {
|
|
40
|
+
if (node.type !== "InputTrigger") return node;
|
|
41
|
+
const cfg = node.data?.config ?? {};
|
|
42
|
+
if (text(cfg.name) || !text(cfg.title)) return node;
|
|
43
|
+
return { ...node, data: { ...node.data, config: { ...cfg, name: kebab(text(cfg.title)) } } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The Task a Task Start is, or undefined when it is not one yet (no title, or nothing saying what
|
|
48
|
+
* it does, or its workflow belongs to no org's app). An Agent file that already names this start
|
|
49
|
+
* IS its Task, so the start reuses that file's tool rather than minting a second one.
|
|
50
|
+
*/
|
|
51
|
+
export function taskOfStart(workflowId: string, node: TaskStartNode): AgentFile | undefined {
|
|
52
|
+
if (node.type !== undefined && node.type !== "InputTrigger") return undefined;
|
|
53
|
+
const known = listAgents().find((a) => a.workflow === workflowId && a.trigger === node.id);
|
|
54
|
+
if (known) return known;
|
|
55
|
+
const cfg = node.data?.config ?? {};
|
|
56
|
+
const title = text(cfg.title);
|
|
57
|
+
const description = text(cfg.description);
|
|
58
|
+
const whenToUse = text(cfg.whenToUse);
|
|
59
|
+
const id = taskStartName(node);
|
|
60
|
+
if (!id || (!description && !whenToUse)) return undefined;
|
|
61
|
+
const org = listApps().find((a) => a.binding?.workflow === workflowId)?.org;
|
|
62
|
+
if (!org) return undefined;
|
|
63
|
+
return {
|
|
64
|
+
name: `${org}/${id}`,
|
|
65
|
+
org,
|
|
66
|
+
id,
|
|
67
|
+
title,
|
|
68
|
+
...(description ? { description } : {}),
|
|
69
|
+
...(whenToUse ? { whenToUse } : {}),
|
|
70
|
+
app: AGENT_CARD,
|
|
71
|
+
workflow: workflowId,
|
|
72
|
+
trigger: node.id,
|
|
73
|
+
...(text(cfg.resultFrom) ? { output: text(cfg.resultFrom) } : {}),
|
|
74
|
+
path: "",
|
|
75
|
+
};
|
|
76
|
+
}
|