@yottagraph-app/aether-instructions 1.1.3 → 1.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottagraph-app/aether-instructions",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Cursor rules, commands, and skills for Aether development",
5
5
  "files": [
6
6
  "rules",
package/rules/agents.mdc CHANGED
@@ -168,14 +168,189 @@ Once deployed, the agent is reachable through the Portal Gateway:
168
168
  ```
169
169
  Chat UI (pages/chat.vue)
170
170
  → POST NUXT_PUBLIC_GATEWAY_URL/api/agents/{tenantId}/{agentId}/query
171
- → Portal Gateway proxies to Vertex AI Agent Engine
172
- → Agent runs, returns response
171
+ → Portal Gateway proxies to Vertex AI Agent Engine (streamQuery)
172
+ → Agent runs (may invoke tools, make multiple LLM calls)
173
+ → Gateway collects the ADK event stream, extracts final text
173
174
  → Chat UI displays it
174
175
  ```
175
176
 
176
177
  The gateway URL and tenant ID come from `broadchurch.yaml` (injected as
177
- `NUXT_PUBLIC_GATEWAY_URL` and `NUXT_PUBLIC_TENANT_ORG_ID`). The chat page
178
- discovers available agents from the Portal config endpoint.
178
+ `NUXT_PUBLIC_GATEWAY_URL` and `NUXT_PUBLIC_TENANT_ORG_ID`).
179
+
180
+ ### Agent Discovery
181
+
182
+ The chat page discovers deployed agents by fetching the tenant config:
183
+
184
+ ```
185
+ GET {NUXT_PUBLIC_GATEWAY_URL}/api/config/{NUXT_PUBLIC_TENANT_ORG_ID}
186
+ ```
187
+
188
+ The response includes an `agents` array:
189
+
190
+ ```json
191
+ {
192
+ "agents": [
193
+ { "name": "filing_analyst", "display_name": "Filing Analyst", "engine_id": "1234567890" },
194
+ { "name": "research_bot", "display_name": "Research Bot", "engine_id": "0987654321" }
195
+ ],
196
+ "features": { "chat": true, ... },
197
+ ...
198
+ }
199
+ ```
200
+
201
+ Each agent entry has:
202
+
203
+ | Field | Type | Description |
204
+ |---|---|---|
205
+ | `name` | `string` | Agent directory name (e.g. `filing_analyst`) |
206
+ | `display_name` | `string` | Human-readable name for the UI |
207
+ | `engine_id` | `string` | Vertex AI Agent Engine resource ID — used as `{agentId}` in query/stream URLs |
208
+
209
+ The `engine_id` is the key value — it becomes the `{agentId}` path
210
+ parameter in `POST /api/agents/{tenantId}/{agentId}/query`.
211
+
212
+ **How agents get populated:** The portal discovers agents from two sources:
213
+ 1. **Firestore** — agents registered by the deploy workflow (`deploy-agent.yml`
214
+ calls `POST /api/agents/{tenantId}` to register)
215
+ 2. **Agent Engine API** — the portal also queries Vertex AI for reasoning
216
+ engines whose display name starts with the project name (e.g.
217
+ `my-project--filing_analyst`), catching agents that were deployed but
218
+ not yet registered
219
+
220
+ Both sources are merged and deduplicated by name. If the config endpoint
221
+ returns an empty `agents` array, no agents have been deployed yet.
222
+
223
+ Use `useTenantConfig()` to fetch this config from Vue code:
224
+
225
+ ```typescript
226
+ import { useTenantConfig } from '~/composables/useTenantConfig';
227
+
228
+ const { config, fetchConfig } = useTenantConfig();
229
+ await fetchConfig();
230
+
231
+ const agents = config.value?.agents ?? [];
232
+ const agentId = agents[0]?.engine_id; // use as {agentId} in query URLs
233
+ ```
234
+
235
+ ### Gateway Request
236
+
237
+ ```
238
+ POST {NUXT_PUBLIC_GATEWAY_URL}/api/agents/{tenantId}/{agentId}/query
239
+ Content-Type: application/json
240
+
241
+ { "message": "Summarize the latest 8-K filing", "session_id": "optional-session-id" }
242
+ ```
243
+
244
+ Omit `session_id` on the first message — the gateway auto-creates one.
245
+
246
+ ### Gateway Response Format
247
+
248
+ ```json
249
+ {
250
+ "output": "The agent's final text response",
251
+ "session_id": "session-abc-123",
252
+ "events": [ /* raw ADK event stream */ ]
253
+ }
254
+ ```
255
+
256
+ | Field | Type | Description |
257
+ |---|---|---|
258
+ | `output` | `string \| any[]` | Usually the agent's final text. Falls back to the raw `events` array if the gateway couldn't extract text. |
259
+ | `session_id` | `string` | Pass back on subsequent messages to continue the conversation. |
260
+ | `events` | `any[]` | Full ADK event stream. Useful for debugging or building UIs that show intermediate agent steps. |
261
+
262
+ **Important:** `output` is NOT always a string. When the agent's response
263
+ involves complex tool chains, the gateway's server-side extraction may miss
264
+ the text and return the raw events array instead. Always use
265
+ `extractAgentText()` to parse `output` safely rather than treating it as a
266
+ string directly.
267
+
268
+ ### ADK Event Stream Format
269
+
270
+ The `events` array contains one object per step the agent took. Each event
271
+ has `content.parts[]` where each part is one of:
272
+
273
+ ```json
274
+ { "text": "The agent's text response..." }
275
+ { "functionCall": { "name": "search", "args": { "q": "AAPL" } } }
276
+ { "functionResponse": { "name": "search", "response": { "results": [...] } } }
277
+ ```
278
+
279
+ A typical stream for an agent that uses a tool:
280
+
281
+ ```json
282
+ [
283
+ { "content": { "parts": [{ "functionCall": { "name": "search", "args": {"q": "AAPL 8-K"} } }], "role": "model" } },
284
+ { "content": { "parts": [{ "functionResponse": { "name": "search", "response": {"results": ["..."]} } }], "role": "tool" } },
285
+ { "content": { "parts": [{ "text": "Here is the summary of the 8-K filing..." }], "role": "model" } }
286
+ ]
287
+ ```
288
+
289
+ The final text is the last `text` part that isn't in a `functionCall` or
290
+ `functionResponse` event. Events may also arrive as JSON strings rather
291
+ than objects — always handle both.
292
+
293
+ ### Parsing Agent Responses (Non-Streaming)
294
+
295
+ For one-shot calls (server routes, background tasks) where streaming isn't
296
+ needed, use the buffered `/query` endpoint with `extractAgentText`:
297
+
298
+ ```typescript
299
+ import { extractAgentText } from '~/composables/useAgentChat';
300
+
301
+ const response = await $fetch(url, { method: 'POST', body: { message } });
302
+ const text = extractAgentText(response.output);
303
+ ```
304
+
305
+ `extractAgentText` handles: plain strings, ADK event stream arrays (with
306
+ JSON-string or object elements), single event objects, and several legacy
307
+ Agent Engine response shapes. It skips `functionCall` / `functionResponse`
308
+ events and returns the agent's final text.
309
+
310
+ ### Streaming Responses
311
+
312
+ The gateway also exposes a streaming endpoint that returns Server-Sent
313
+ Events as the agent executes. **The `useAgentChat` composable uses this by
314
+ default** — it tries `/stream` first and falls back to `/query`
315
+ automatically.
316
+
317
+ ```
318
+ POST {NUXT_PUBLIC_GATEWAY_URL}/api/agents/{tenantId}/{agentId}/stream
319
+ Content-Type: application/json
320
+
321
+ { "message": "Summarize the latest 8-K filing", "session_id": "optional" }
322
+ ```
323
+
324
+ The response is an SSE stream with these event types:
325
+
326
+ | Event | Data Shape | Description |
327
+ |---|---|---|
328
+ | `text` | `{ "text": "..." }` | Agent text output (replaces previous text) |
329
+ | `function_call` | `{ "name": "...", "args": {...} }` | Agent is calling a tool |
330
+ | `function_response` | `{ "name": "...", "response": {...} }` | Tool returned a result |
331
+ | `error` | `{ "message": "..." }` | Error during processing |
332
+ | `done` | `{ "session_id": "...", "text": "..." }` | Stream complete with final text |
333
+
334
+ For custom agent UIs that need streaming, import `readSSE`:
335
+
336
+ ```typescript
337
+ import { readSSE } from '~/composables/useAgentChat';
338
+
339
+ const res = await fetch(streamUrl, {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify({ message: 'Hello' }),
343
+ });
344
+
345
+ for await (const { event, data } of readSSE(res)) {
346
+ if (event === 'text') console.log('Agent says:', data.text);
347
+ if (event === 'function_call') console.log('Calling:', data.name);
348
+ if (event === 'done') console.log('Session:', data.session_id);
349
+ }
350
+ ```
351
+
352
+ The `done` event always includes the final extracted text, so you don't
353
+ need to track text deltas yourself.
179
354
 
180
355
  ## Agent Design Guidelines
181
356
 
package/rules/api.mdc CHANGED
@@ -20,7 +20,9 @@ For full endpoint documentation, read the **elemental-api skill** in
20
20
  `skills/elemental-api/`. Start with `SKILL.md`, then `overview.md`.
21
21
  These files are copied from `@yottagraph-app/elemental-api-skill` during
22
22
  `npm install` (postinstall step) — if the directory is empty, run
23
- `npm install` first.
23
+ `npm install` first. The skill docs contain detailed response shapes and
24
+ edge cases that go beyond this rule's quick reference — **run `npm install`
25
+ early** to make them available during initial exploration.
24
26
 
25
27
  Key files:
26
28
  - `entities.md` — entity search, details, and properties
@@ -51,6 +53,57 @@ Types are also imported from the client:
51
53
  import type { NamedEntityReport, GetNEIDResponse } from '@yottagraph-app/elemental-api/client';
52
54
  ```
53
55
 
56
+ ### Client Method Quick Reference
57
+
58
+ All methods return data directly and throw on non-2xx responses.
59
+
60
+ **Entity search and lookup:**
61
+
62
+ | Method | Signature | Purpose |
63
+ |---|---|---|
64
+ | `getNEID` | `(params: { entityName, maxResults?, includeNames? })` | Lookup entity by name |
65
+ | `findEntities` | `(body: FindEntitiesBody)` | Expression-based search (see `find.md`) |
66
+ | `getNamedEntityReport` | `(neid: string)` | Entity details (name, aliases, type) |
67
+ | `getEntityDetails` | `(neid: string)` | Alias for entity reports |
68
+
69
+ **Properties and schema:**
70
+
71
+ | Method | Signature | Purpose |
72
+ |---|---|---|
73
+ | `getSchema` | `()` | All entity types (flavors) and properties (PIDs) |
74
+ | `getPropertyValues` | `(body: { eids: string, pids: string })` | Property values (eids/pids are JSON-stringified arrays!) |
75
+ | `summarizeProperty` | `(pid: number)` | Summary stats for a property |
76
+
77
+ **Relationships and graph:**
78
+
79
+ | Method | Signature | Purpose |
80
+ |---|---|---|
81
+ | `getLinkedEntities` | `(neid, params?: { entity_type?, link_type? })` | Linked entities (person/org/location only) |
82
+ | `getLinks` | `(sourceNeid, targetNeid, params?)` | Links between two specific entities |
83
+ | `getLinkCounts` | `(sourceNeid, targetNeid)` | Link counts between entities |
84
+ | `getNeighborhood` | `(centerNeid, params?)` | Neighboring entities |
85
+ | `getGraphLayout` | `(centerNeid, params?)` | Graph layout for visualization |
86
+
87
+ **News, events, and sentiment:**
88
+
89
+ | Method | Signature | Purpose |
90
+ |---|---|---|
91
+ | `getArticle` | `(artid: string)` | Article by ID |
92
+ | `getArticleText` | `(artid: string)` | Article full text |
93
+ | `getEvent` | `(eveid: string)` | Event by ID |
94
+ | `getEventsForEntity` | `(params: { neid, startTime?, endTime? })` | Events involving an entity |
95
+ | `getMentions` | `(params: { neid, startTime?, endTime? })` | Mention codes for entities |
96
+ | `getMentionCounts` | `(params: { neid, ... })` | Bucketed mention counts |
97
+ | `getNamedEntitySentiment` | `(neid: string)` | Sentiment for an entity |
98
+
99
+ **Other:**
100
+
101
+ | Method | Signature | Purpose |
102
+ |---|---|---|
103
+ | `getHealth` | `()` | Health check |
104
+ | `getStatus` | `()` | Server status and capabilities |
105
+ | `adaMessage` | `(body: AdaMessageBody)` | Ada AI chat |
106
+
54
107
  ## Discovery-First Pattern
55
108
 
56
109
  The knowledge graph contains many entity types and properties, and new datasets
@@ -81,17 +134,49 @@ knowledge of what's in the graph.
81
134
 
82
135
  ## API Gotchas
83
136
 
84
- > **WARNING -- `getSchema()` response nesting**: The generated TypeScript types
85
- > put `flavors` and `properties` at the top level, but the actual API response
86
- > nests them under a `schema` key. Using `response.properties` directly will
87
- > crash with `Cannot read properties of undefined`. Always use:
137
+ ### `getSchema()` response is nested WILL crash if you don't handle it
138
+
139
+ The generated TypeScript types suggest `response.properties` and
140
+ `response.flavors` exist at the top level. **They don't.** The API nests
141
+ them under `response.schema`. This mismatch between types and reality
142
+ causes `Cannot read properties of undefined` every time.
88
143
 
89
144
  ```typescript
145
+ // WRONG — will crash at runtime despite TypeScript compiling fine:
146
+ const res = await client.getSchema();
147
+ const props = res.properties; // undefined!
148
+
149
+ // CORRECT — always access through .schema:
90
150
  const res = await client.getSchema();
91
151
  const properties = res.schema?.properties ?? (res as any).properties ?? [];
92
152
  const flavors = res.schema?.flavors ?? (res as any).flavors ?? [];
93
153
  ```
94
154
 
155
+ The `(res as any).properties` fallback is there in case the API is ever
156
+ fixed to match the types. Use this pattern every time.
157
+
158
+ ### `getNamedEntityReport()` response is nested under `.report`
159
+
160
+ Same problem as `getSchema()`. The TypeScript types suggest entity fields
161
+ (`name`, `aliases`, `type`) exist at the top level. **They don't.** The
162
+ API wraps them in a `.report` container. The generated client does NOT
163
+ unwrap this automatically.
164
+
165
+ ```typescript
166
+ // WRONG — name will be undefined:
167
+ const res = await client.getNamedEntityReport(neid);
168
+ const name = res.name; // undefined!
169
+
170
+ // CORRECT — always access through .report:
171
+ const res = await client.getNamedEntityReport(neid);
172
+ const name = res.report?.name ?? (res as any).name ?? neid;
173
+ const aliases = res.report?.aliases ?? (res as any).aliases ?? [];
174
+ const type = res.report?.type ?? (res as any).type;
175
+ ```
176
+
177
+ The `(res as any).name` fallback handles the case where the client is
178
+ eventually fixed to unwrap the response.
179
+
95
180
  > **WARNING -- `getPropertyValues()` takes JSON-stringified arrays**: The `eids`
96
181
  > and `pids` parameters must be JSON-encoded strings, NOT native arrays. The
97
182
  > TypeScript type is `string`, not `string[]`. Passing a raw array will silently
@@ -114,6 +199,17 @@ financial instruments, events, and all other types are excluded — even
114
199
  though the schema shows relationships like `filed` connecting organizations
115
200
  to documents.
116
201
 
202
+ **Why?** The knowledge graph has two layers. The **graph layer** models
203
+ first-class entities (people, organizations, locations) as nodes with
204
+ edges between them — this is what `getLinkedEntities` traverses. The
205
+ **property layer** attaches everything else (documents, filings, financial
206
+ instruments, events) as property values on graph nodes. Documents aren't
207
+ "lesser" entities — they're stored differently because they're associated
208
+ with specific graph nodes rather than standing independently in the graph.
209
+ Understanding this distinction helps you generalize: if a target entity
210
+ type doesn't appear in the `getLinkedEntities` response, it's a property-
211
+ layer entity and you need `getPropertyValues` with the relationship PID.
212
+
117
213
  To traverse relationships to non-graph-node types, use `getPropertyValues`
118
214
  with the relationship PID instead. Relationship properties (`data_nindex`)
119
215
  return linked entity IDs as values. Zero-pad the returned IDs to 20
@@ -139,6 +235,51 @@ See the **cookbook** rule for a full "Get filings for a company" recipe.
139
235
  (`POST /elemental/find`). Best for filtered searches (by type, property,
140
236
  relationship). See `find.md` for the expression language.
141
237
 
238
+ ## Common Entity Relationships
239
+
240
+ The knowledge graph connects entities through relationship properties
241
+ (`data_nindex` PIDs). These are the most common patterns:
242
+
243
+ | From | PID | To | How to traverse |
244
+ |---|---|---|---|
245
+ | Organization | `filed` | Document (filings) | `getPropertyValues` with the `filed` PID on the org's NEID |
246
+ | Organization | `employs` | Person | `getLinkedEntities` (person is a graph node type) |
247
+ | Person | `employed_by` | Organization | `getLinkedEntities` (organization is a graph node type) |
248
+ | Organization | `headquartered_in` | Location | `getLinkedEntities` (location is a graph node type) |
249
+ | Any entity | `related_to` | Any entity | `getLinkedEntities` for person/org/location; `getPropertyValues` for others |
250
+
251
+ **Key constraint:** `getLinkedEntities` only works for three target types:
252
+ **person**, **organization**, **location**. For documents, filings,
253
+ articles, financial instruments, and events, use `getPropertyValues` with
254
+ the relationship PID. See the cookbook rule (recipe #7) for a full filings
255
+ example.
256
+
257
+ **Traversal pattern for non-graph-node types:**
258
+
259
+ ```typescript
260
+ // 1. Get the PID for the relationship
261
+ const schema = await client.getSchema();
262
+ const pids = schema.schema?.properties ?? [];
263
+ const filedPid = pids.find((p: any) => p.name === 'filed')?.pid;
264
+
265
+ // 2. Get linked entity IDs via getPropertyValues
266
+ const res = await client.getPropertyValues({
267
+ eids: JSON.stringify([orgNeid]),
268
+ pids: JSON.stringify([filedPid]),
269
+ });
270
+
271
+ // 3. Pad IDs to 20 chars to form valid NEIDs
272
+ const docNeids = res.values.map((v: any) => String(v.value).padStart(20, '0'));
273
+
274
+ // 4. Get details for each linked entity (response is nested under .report)
275
+ const reports = await Promise.all(
276
+ docNeids.map(async (neid: string) => {
277
+ const r = await client.getNamedEntityReport(neid);
278
+ return r.report ?? r;
279
+ }),
280
+ );
281
+ ```
282
+
142
283
  ## Error Handling
143
284
 
144
285
  ```typescript
@@ -133,9 +133,21 @@ Tenant-specific configuration generated during provisioning. Contains GCP projec
133
133
 
134
134
  ## Built-in Pages
135
135
 
136
- These pages ship with the template and can be kept, modified, or removed based on the app's needs:
137
-
138
- - `pages/chat.vue` -- Agent chat UI. Talks to deployed ADK agents through the Portal Gateway. Keep if the app uses AI agents.
139
- - `pages/mcp.vue` -- MCP Explorer. Browse and test MCP server tools. Keep if the app uses MCP servers.
140
- - `pages/entity-lookup.vue` -- Entity search tool. Useful for looking up NEIDs. Keep or remove based on the app.
136
+ Recent template versions include these pages. They can be kept, modified,
137
+ or removed based on the app's needs:
138
+
139
+ - `pages/chat.vue` -- Agent chat UI. Uses `useAgentChat()` and
140
+ `useTenantConfig()` to discover deployed agents and stream responses
141
+ through the Portal Gateway. Keep if the app uses AI agents.
142
+ - `pages/mcp.vue` -- MCP Explorer. Browse and test MCP server tools. Keep
143
+ if the app uses MCP servers.
144
+ - `pages/entity-lookup.vue` -- Entity search tool. Useful for looking up
145
+ NEIDs. Keep or remove based on the app.
146
+
147
+ **If these pages are missing** from your project, your project was created
148
+ from an older template version. Create them from scratch — they're
149
+ straightforward Vuetify pages. The key composables (`useAgentChat`,
150
+ `useTenantConfig`) are included in all template versions and provide the
151
+ connection logic. See the `agents` cursor rule for the gateway endpoints
152
+ and response formats these pages use.
141
153
 
@@ -467,7 +467,7 @@ NOT supported — use `getPropertyValues` with the relationship PID instead.
467
467
  docNeids.map(async (neid: string) => {
468
468
  try {
469
469
  const r = await client.getNamedEntityReport(neid);
470
- return r.name || neid;
470
+ return r.report?.name ?? (r as any).name ?? neid;
471
471
  } catch {
472
472
  return neid;
473
473
  }