You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0160797
Browse filesBrowse the repository at this point in the historyBrowse files
||**Backup**| GET `/settings/backup`, POST `/settings/restore` (tar.gz of data/) |
192
193
||**Voice**| GET `/voice/types` (registered voice providers with JSON Schemas) |
@@ -236,7 +237,7 @@ log:
236
237
- **MCP headers/TLS**: `MCPServer` struct has `Headers map[string]string` and `Insecure bool`. `httpClientForMCP()` creates transport with optional `InsecureSkipVerify`
237
238
- **Skill injection**: Skills are injected into the agent system prompt at build time. Instructions appended as `--- Skill: {name} ---`, reference file contents appended as `[Reference: {filename}]`. Files read from `data/skills/{skillId}/`
238
239
- **Encryption key**: `server.encryptionKey` in config.yaml. Independent from `adminPassword`. Used to encrypt secrets at rest (AES-256-GCM, PBKDF2-derived)
239
-
- **ContextGuard plugin**: Externalized to `adk-utils-go/plugin/contextguard` (v0.13.0). Builder API: `contextguard.New(registry)` + `guard.Add(agentID, llm, opts...)` + `guard.PluginConfig()`. Two strategies: `threshold` (token-based, auto-detect via CrushRegistry or manual `WithMaxTokens`) and `sliding_window` (turn-count via `WithSlidingWindow`). Each agent summarizes with its own LLM. Summary persisted in session state with `{agentName}` suffix keys. `CrushRegistry` fetches model metadata from Crush's provider.json with 6h background refresh
240
+
- **ContextGuard plugin**: Externalized to `adk-utils-go/plugin/contextguard` (v0.15.2). Builder API: `contextguard.New(registry)` + `guard.Add(agentID, llm, opts...)` + `guard.PluginConfig()`. Two strategies: `threshold` (token-based, auto-detect via CrushRegistry or manual `WithMaxTokens`) and `sliding_window` (turn-count via `WithSlidingWindow`). Each agent summarizes with its own LLM. Summary persisted in session state with `{agentName}` suffix keys. `CrushRegistry` fetches model metadata from Crush's provider.json with 6h background refresh
240
241
- **A2A protocol**: Agents/flows with `A2A.Enabled` get JSON-RPC endpoints via `a2a-go` + ADK `adka2a`. Agent cards auto-generated with capabilities and skills. SSE streaming for responses
241
242
- **Dual-perspective conversation recording**: Middleware chains recorder twice: "admin" perspective (all events, before FlowResponseFilter) and "user" perspective (filtered, after). Each conversation has a `ParentID` linking the pair
242
243
- **Store dual-copy pattern**: Store maintains `rawData` (unexpanded, with `${VAR}` refs) and `data` (env-expanded). API responses use raw data, runtime uses expanded. Secret values injected as env vars before expansion
@@ -245,7 +246,8 @@ log:
245
246
- **InstructionProvider pattern**: Agents use `InstructionProvider` instead of static `Instruction` strings to bypass ADK's built-in `{variable}` substitution (which conflicts with curly braces in prompts, JSON examples, scripts, etc). Magec resolves its own `{{agent.output:key}}` pattern from session state inside the provider. Plain `{text}` in prompts is never touched
246
247
- **Flow wrapAgent pattern**: Same agent can appear in multiple flow steps — `wrapAgent()` creates uniquely-named delegate agents to satisfy ADK's single-parent constraint
247
248
- **Voice provider registry**: TTS/STT proxies dispatch to per-backend-type providers via `voice.Get(backend.Type)`. Same pattern as clients/memory — `init()` + blank imports. OpenAI provider handles `/v1/audio/speech` and `/v1/audio/transcriptions`. Gemini provider translates to `generateContent` with `speechConfig`/`inlineData`. `TTSRef.Config` and `BackendRef.Config` use typed structs (`TTSConfig`, `STTConfig`) with per-provider namespaces matching the `ClientConfig` pattern (e.g. `config.openai.speed`, `config.gemini.languageCode`). `GET /voice/types` returns JSON Schemas per provider. Store migration moves legacy `tts.speed` → `tts.config.openai.speed` and flat config fields → `tts.config.gemini.*`
248
-
- **Input artifact offloading**: User-uploaded files smaller than `msgutil.DefaultInlineThreshold` (1 MiB) ride inline in the `/run_sse` body as `inlineData` parts. Larger files are persisted through the ADK `artifact.Service` and replaced in the prompt with a `MAGEC_ATTACHED_ARTIFACTS` block telling the model to call `load_artifact` on demand. Reuses the universal artifact toolset (decision #17). The service is injected per-client through `SetArtifactService` and sourced from `agentRouterHandler.ArtifactService()` so it tracks store rebuilds. Helpers in `server/clients/msgutil/attachments.go`: `ShouldInline`, `InlinePart`, `StoreAsArtifact`, `AttachedArtifactsBlock` — each client runs its own short loop over platform-specific attachment types.
249
+
- **Input artifact offloading**: User-uploaded files are persisted through the ADK `artifact.Service` and replaced in the prompt with a `MAGEC_ATTACHED_ARTIFACTS` block telling the model to call `load_artifact` on demand. Reuses the universal artifact toolset (decision #17). The service is injected per-client through `SetArtifactService` and sourced from `agentRouterHandler.ArtifactService()` so it tracks store rebuilds. Helpers in `server/clients/msgutil/attachments.go`: `StoreAsArtifact`, `AttachedArtifactsBlock` — each client runs its own short loop over platform-specific attachment types.
250
+
- **Artifact toolset**: universal via `base_toolset.go` (decision #17). Exposes `save_artifact`, `load_artifact`, `list_artifacts`, `export_artifact` and `get_artifact_url`. `load_artifact` injects the artifact as a native multimodal `*genai.Part` via `ProcessRequest` rather than serialising base64 (decision #25). `export_artifact` writes raw bytes to disk under `Store.ResolveTemporaryDir()` and returns the absolute path so non-artifact-aware tools running in the same container can pick the file up (decision #26). `get_artifact_url` mints a short-lived HMAC-signed URL under `/api/v1/ephemeral/artifacts/{token}` so consumers in other processes/containers can fetch the artifact's raw bytes over HTTP without sharing volumes (decision #27); the tool stays unregistered when `server.encryptionKey` is unset. `Store.ResolveTemporaryDir()` is the single fallback point for `Settings.TemporaryDir` → `os.TempDir()`; nobody else may compute that fallback.
249
251
250
252
### Design Philosophy
251
253
@@ -266,7 +268,7 @@ log:
266
268
- **Tailwind v4**: `@tailwindcss/vite` plugin, `@theme` directive for custom colors
- **Settings view**: Global memory provider selection + backup/restore (tar.gz)
271
+
- **Settings view**: Runtime (`temporaryDir` for transient files used by tools like `export_artifact`) + backup/restore (tar.gz). Memory provider selection lives in the global settings struct but is not yet exposed in the UI.
270
272
271
273
### Frontend Conventions (voice-ui)
272
274
@@ -318,7 +320,7 @@ GPU section commented out by default. Users who want cloud providers create diff
318
320
319
321
- `google.golang.org/adk`— Agent Development Kit (v1.0.0)
320
322
- `google.golang.org/genai`— Google GenAI SDK (v1.40.0)
@@ -354,10 +356,11 @@ GPU section commented out by default. Users who want cloud providers create diff
354
356
13. **Git branch is `master`**, not `main`. All raw GitHub URLs use `master`.
355
357
14. **Go 1.25+, Node 22+, Hugo v0.155+**.
356
358
15. **A2A agent card endpoints bypass client auth**: `.well-known/agent-card.json`paths are exempted from `ClientAuth` middleware so external agents can discover cards.
357
-
16. **ContextGuard `safeSplitIndex`**: When splitting conversation history for summarization, the split point is adjusted to avoid orphaning Anthropic `tool_result` blocks.
358
-
17. **Store env var expansion**: All store fields support `${VAR}` syntax. Secrets are injected as env vars (`os.Setenv`) before the store is expanded, so secrets can be referenced in backend URLs, bot tokens, etc.
359
-
18. **Voice API routes always registered**: STT/TTS proxy endpoints are available regardless of Voice UI toggle, since Telegram/Discord/Slack clients need them.
360
-
19. **ADK REST API accepts both camelCase and snake_case**: The `SnakeCaseNormalize` middleware converts snake_case keys recursively before ADK sees the request. This applies only to `/run` and `/run_sse` — the only ADK endpoints with multi-word JSON body fields. Session create/get/delete use single-word body fields (`state`, `events`) or path parameters only.
359
+
16. **Ephemeral artifact URLs bypass client auth**: `/api/v1/ephemeral/*`paths are exempted from `ClientAuth` because the HMAC-SHA256 signed token in the URL is the credential. Tokens are minted by the `get_artifact_url` tool and verified server-side with `server.encryptionKey`. See decision #27.
360
+
17. **ContextGuard `safeSplitIndex`**: When splitting conversation history for summarization, the split point is adjusted to avoid orphaning Anthropic `tool_result` blocks.
361
+
18. **Store env var expansion**: All store fields support `${VAR}` syntax. Secrets are injected as env vars (`os.Setenv`) before the store is expanded, so secrets can be referenced in backend URLs, bot tokens, etc.
362
+
19. **Voice API routes always registered**: STT/TTS proxy endpoints are available regardless of Voice UI toggle, since Telegram/Discord/Slack clients need them.
363
+
20. **ADK REST API accepts both camelCase and snake_case**: The `SnakeCaseNormalize` middleware converts snake_case keys recursively before ADK sees the request. This applies only to `/run` and `/run_sse` — the only ADK endpoints with multi-word JSON body fields. Session create/get/delete use single-word body fields (`state`, `events`) or path parameters only.
Copy file name to clipboardExpand all lines: .agents/CLIENT_DESIGN.md
+10-17Lines changed: 10 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -450,17 +450,14 @@ On `loadFromDisk()`, these migrations run in order (all idempotent):
450
450
451
451
### Overview
452
452
453
-
Telegram, Slack and Discord clients accept user-uploaded files and forward them to the ADK `/run_sse` request. The adapter chooses between two paths based on size, using the helpers in `server/clients/msgutil/attachments.go`:
453
+
Telegram, Slack and Discord clients accept user-uploaded files and forward them to the ADK `/run_sse` request. The adapter processes them using helpers in `server/clients/msgutil/attachments.go`:
454
454
455
-
-**Inline** — files at or below `msgutil.DefaultInlineThreshold` (1 MiB) ride in the request body as `inlineData` parts next to the user text. Fastest path, no tool calls required.
456
-
-**Artifact** — files above the threshold are persisted through the ADK `artifact.Service` with a session-unique filename (`{messageID}_{originalName}`). The prompt is amended with a `MAGEC_ATTACHED_ARTIFACTS` HTML-comment block listing each file (name, MIME, human size) and instructing the model to call `load_artifact` on demand.
455
+
-**Artifact** — files are persisted through the ADK `artifact.Service` with a session-unique filename (`{messageID}_{originalName}`). The prompt is amended with a `MAGEC_ATTACHED_ARTIFACTS` HTML-comment block listing each file (name, MIME, human size) and instructing the model to call `load_artifact` on demand.
457
456
458
457
See decision #24 in `.agents/DECISIONS.md` for the rationale and "do not" rules.
-`InlinePart(mimeType string, data []byte) map[string]interface{}` — builds the `inlineData` map that ADK serializes to `genai.Part`.
464
461
-`StoreAsArtifact(ctx, svc, appName, userID, sessionID, filename, mimeType, data) (descriptor, error)` — persists via `artifact.Service.Save` and returns a single descriptor line meant to be wrapped by `AttachedArtifactsBlock`.
465
462
-`AttachedArtifactsBlock(lines []string) string` — wraps descriptor lines in the `MAGEC_ATTACHED_ARTIFACTS` block; returns `""` on empty input so callers can concatenate unconditionally.
466
463
@@ -472,19 +469,16 @@ Each client has its own short loop over platform-specific attachment types (`msg
472
469
User sends file in Telegram/Slack/Discord
473
470
→ Client downloads bytes from platform API (per-platform size cap: 20–25 MiB)
→ POST /run_sse with {parts: [{text: prompt}, ...inline]}
482
-
→ LLM either reads inline content or calls load_artifact for the rest
475
+
→ POST /run_sse with {parts: [{text: prompt}]}
476
+
→ LLM calls load_artifact when it needs to read the file contents
483
477
```
484
478
485
479
### Fallback
486
480
487
-
When `artifacts == nil` (no agents configured yet, or intentionally unset) the policy inlines everything regardless of size. When a `StoreAsArtifact` call fails (disk error, service down), the client logs a warning and falls back to inline for that file — the user message is still delivered.
481
+
When `artifacts == nil` (no agents configured yet, or intentionally unset), the file is dropped with a warning. When a `StoreAsArtifact` call fails (disk error, service down), the client logs a warning and drops that file — the text part of the user message is still delivered.
488
482
489
483
### Platform specifics
490
484
@@ -496,12 +490,11 @@ When `artifacts == nil` (no agents configured yet, or intentionally unset) the p
496
490
497
491
### Parts serialization
498
492
499
-
Clients construct parts as raw `map[string]interface{}` (not `genai` types) since they serialize directly to JSON for the HTTP call to ADK. `msgutil.InlinePart` returns exactly this shape, so callers just append its result:
493
+
Clients construct parts as raw `map[string]interface{}` (not `genai` types) since they serialize directly to JSON for the HTTP call to ADK:
0 commit comments