@dotdrelle/wiki-manager 0.14.16 → 0.14.23

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.
Files changed (46) hide show
  1. package/.env.example +46 -4
  2. package/README.md +116 -11
  3. package/agents.docker-compose.yml +41 -0
  4. package/docker-compose.yml +16 -1
  5. package/mcp.endpoints.example.json +3 -3
  6. package/package.json +1 -2
  7. package/src/activity/activityAggregator.js +7 -3
  8. package/src/activity/activityAggregator.test.js +16 -2
  9. package/src/agent/graph.js +91 -11
  10. package/src/agent/graph.test.js +108 -6
  11. package/src/cli/runtimeStartup.test.js +14 -0
  12. package/src/cli/wiki-manager.js +220 -37
  13. package/src/cli/wiki-manager.test.js +131 -1
  14. package/src/commands/slash.js +58 -0
  15. package/src/commands/slash.test.js +18 -0
  16. package/src/core/activity.js +1 -1
  17. package/src/core/activity.test.js +5 -0
  18. package/src/core/buildInfo.json +2 -2
  19. package/src/core/compose.js +7 -1
  20. package/src/core/dockerCompose.test.js +27 -3
  21. package/src/core/env.js +16 -3
  22. package/src/core/env.test.js +8 -3
  23. package/src/core/mcp.js +7 -2
  24. package/src/core/mcp.test.js +55 -0
  25. package/src/core/wikiSetup.js +26 -2
  26. package/src/core/wikiWorkspace.test.js +25 -0
  27. package/src/core/workflow.js +72 -0
  28. package/src/core/workflow.test.js +57 -0
  29. package/src/orchestrator/dispatcher.js +1 -1
  30. package/src/orchestrator/dispatcher.test.js +48 -0
  31. package/src/orchestrator/scheduler.js +27 -7
  32. package/src/orchestrator/scheduler.test.js +23 -0
  33. package/src/runtime/client.js +23 -0
  34. package/src/runtime/runner.js +30 -3
  35. package/src/runtime/store.js +54 -0
  36. package/src/runtime/store.test.js +42 -0
  37. package/src/shell/LeftPane.tsx +20 -1
  38. package/src/shell/RightPane.tsx +22 -3
  39. package/src/shell/repl.js +86 -9
  40. package/src/shell/repl.test.js +103 -2
  41. package/src/shell/tui.tsx +6 -1
  42. package/src/shell/useAgent.ts +8 -12
  43. package/src/shell/useSession.ts +47 -0
  44. package/tsconfig.json +2 -1
  45. package/wiki-workspace +110 -18
  46. package/agents.docker-compose.mailer.example.yml +0 -36
package/.env.example CHANGED
@@ -42,11 +42,39 @@ WORKSPACES_ROOT=/path/to/workspaces
42
42
 
43
43
  CME_MCP_AUTH_TOKEN=
44
44
  DOCUMENTS_MCP_AUTH_TOKEN=
45
+ # Generated by `wiki-workspace agents up` when missing.
46
+ CONNECTORS_MCP_AUTH_TOKEN=
45
47
 
46
48
  # ── Agent ports (optional, change only if defaults conflict) ───────────────────
47
49
 
48
50
  # CME_MCP_PORT=3336
49
51
  # DOCUMENTS_MCP_PORT=3337
52
+ # CONNECTORS_MCP_PORT=3338
53
+
54
+ # ── SaaS connectors (optional) ────────────────────────────────────────────────
55
+ #
56
+ # Enable the opt-in agent-connectors service:
57
+ CONNECTORS_ENABLED=false
58
+ #
59
+ # The public wikiLLM Desktop/PKCE Client ID is built into agent-connectors.
60
+ # Optional advanced/self-hosted override:
61
+ # GOOGLE_OAUTH_CLIENT_ID=
62
+ # Optional confidential-client compatibility override:
63
+ # GOOGLE_OAUTH_CLIENT_SECRET=
64
+ #
65
+ # The callback URL is generated automatically
66
+ # by `wiki-workspace agents up` when empty. Register the resulting exact URL in
67
+ # Google Cloud Console. Override it only for a remote/public HTTPS deployment.
68
+ # GOOGLE_OAUTH_CALLBACK_URL=http://127.0.0.1:${CONNECTORS_MCP_PORT}/oauth/google/callback
69
+ #
70
+ # Generated automatically when missing. Keep the two secrets distinct.
71
+ # OAUTH_STATE_SECRET=
72
+ # OAUTH_START_TOKEN=
73
+ # OAUTH_STATE_TTL_SECONDS=600
74
+ #
75
+ # Collection concurrency advertised to Donna:
76
+ # CONNECTORS_RECOMMENDED_CONCURRENCY=2
77
+ # CONNECTORS_MAX_CONCURRENCY=4
50
78
 
51
79
  # ── Documents LLM OCR / Mermaid (optional) ─────────────────────────────────────
52
80
 
@@ -74,10 +102,24 @@ DOCUMENTS_MCP_AUTH_TOKEN=
74
102
  # WIKI_MANAGER_RUNTIME_HOST=0.0.0.0
75
103
 
76
104
 
77
- # Parallel tasks dispatched at once for capability runs. Defaults to what the
78
- # agent itself declares (agent_describe limits); set only to constrain it.
79
- # Example constraint (never raises an agent's declared capacity):
80
- # WIKI_MANAGER_CAPABILITY_CONCURRENCY=3
105
+ # ── Parallelism & throughput ───────────────────────────────────────────────────
106
+ # Effective concurrency = MIN(agent recommendedConcurrency, agent maxConcurrency,
107
+ # this ceiling, per-task limits). So the PRIMARY levers live on the production
108
+ # agent (PRODUCTION_RECOMMENDED_CONCURRENCY / PRODUCTION_MAX_CONCURRENCY); this
109
+ # manager variable can only LOWER the result, never raise it. Full explanation
110
+ # and low/high profiles in docs/configuration.md § "Parallelism & throughput".
111
+ #
112
+ # Manager ceiling — leave unset to let the agent decide. Set to constrain:
113
+ # WIKI_MANAGER_CAPABILITY_CONCURRENCY=4
114
+ #
115
+ # Production agent capacity (passed through by docker-compose). Intermediate
116
+ # defaults are 4/8 (effective ≈ 4 parallel). Profiles:
117
+ # low → PRODUCTION_RECOMMENDED_CONCURRENCY=2 PRODUCTION_MAX_CONCURRENCY=4
118
+ # high → PRODUCTION_RECOMMENDED_CONCURRENCY=8 PRODUCTION_MAX_CONCURRENCY=16
119
+ # The wiki LLM backend must accept this many concurrent requests, and
120
+ # ingest_apply stays serialized regardless (global workspace-write lock).
121
+ # PRODUCTION_RECOMMENDED_CONCURRENCY=4
122
+ # PRODUCTION_MAX_CONCURRENCY=8
81
123
 
82
124
  # ── MCP retry policy (optional) ────────────────────────────────────────────────
83
125
 
package/README.md CHANGED
@@ -393,7 +393,6 @@ of you in the browser (create → configure → start the agents → open).
393
393
  | [`agent-cme`](https://github.com/dotdrelle/agent-cme) | Global Confluence to Markdown MCP exporter; workspace injected automatically by Donna |
394
394
  | [`agent-wiki-production`](https://github.com/dotdrelle/agent-wiki-production) | Workspace-scoped production jobs: ingest, build, export, polish, pipeline |
395
395
  | [`agent-wiki-documents`](https://github.com/dotdrelle/agent-wiki-documents) | Document conversion MCP: PDF/Office/HTML/images → Markdown (OCR-capable) |
396
- | [`agent-mailer-api`](https://github.com/dotdrelle/agent-mailer-api) | Optional external mailer MCP endpoint (user-side override, not in the default stack) |
397
396
 
398
397
  ## Workspace Model
399
398
 
@@ -487,7 +486,7 @@ cp .env.example .env
487
486
 
488
487
  The `.env` file is loaded automatically by both `wiki-manager` (Node/Bun process)
489
488
  and `wiki-workspace` (Docker Compose). It sets `WORKSPACES_ROOT`, per-agent auth
490
- tokens and optional port overrides; credentials of optional connectors you enable via the user override.
489
+ tokens, optional port overrides, and credentials for enabled connectors.
491
490
 
492
491
  ### External MCP endpoints
493
492
 
@@ -550,6 +549,25 @@ or the shell command `/approve item <id>`. The approval timeout defaults to 10
550
549
  minutes and can be changed with `WIKI_MANAGER_APPROVAL_TIMEOUT_MS` or
551
550
  `approvalTimeoutMs` in the `/run` body.
552
551
 
552
+ Directly-launched capability runs (ingest, pipeline) now **wait for approval by
553
+ default** before their mutating tasks: reply "valide tout", run `/approve`, or
554
+ click Approve in either UI (Shell right-pane banner, or the `serve` banner above
555
+ the composer). Auto-approval only happens when the run is started with
556
+ `autoApprove: true` (headless/CI).
557
+
558
+ ### Parallelism & throughput
559
+
560
+ The number of tasks that run at once is `MIN(agent recommendedConcurrency, agent
561
+ maxConcurrency, WIKI_MANAGER_CAPABILITY_CONCURRENCY, per-task limits)` — a
562
+ minimum, so the manager ceiling can only lower it. The production agent ships
563
+ intermediate defaults (`PRODUCTION_RECOMMENDED_CONCURRENCY=4` /
564
+ `PRODUCTION_MAX_CONCURRENCY=8`, ≈ 4 parallel); locks then cap real parallelism
565
+ per phase (`ingest_apply` stays serial). The resolved value is shown in both
566
+ UIs' run summary and on the run node of the execution graph, with an amber
567
+ "(ceiling)" marker when the manager ceiling binds. Low/high profiles, the lock
568
+ model and the LLM-backend caveat are in
569
+ [docs/configuration.md § "Parallelism & throughput"](docs/configuration.md).
570
+
553
571
  While a run is active, `GET`/`POST /control` still answers without waiting for
554
572
  it to finish: `{"action":"status"}` returns the current run/plan/queue state,
555
573
  `{"action":"explain"}` adds a one-line plain-language summary, and
@@ -580,18 +598,104 @@ generates the missing agent auth tokens into your manager `.env` and seeds
580
598
  automatically from the manager workspaces directory. Agent state is stored under
581
599
  `./.agents-data/` unless `AGENTS_DATA_DIR` is set.
582
600
 
583
- #### Optional agents and user overrides
601
+ An `npm -g update @dotdrelle/wiki-manager` replaces the packaged Compose files
602
+ but preserves the operator-owned `.env`, `mcp.endpoints.json`, workspaces, agent
603
+ data, and runtime database. Missing standard endpoint definitions are migrated
604
+ additively; existing endpoint definitions are never overwritten.
584
605
 
585
- Anything beyond the default stack (for example the MailerSend agent) is an
586
- external connector operated at the user's charge — built and published, but
587
- not part of the delivery. To enable one, create a file named
588
- `agents.docker-compose.override.yml` **next to your `.env`**:
606
+ The Gmail connector agent is packaged but opt-in. Enable it in the manager
607
+ `.env`:
608
+
609
+ ```dotenv
610
+ CONNECTORS_ENABLED=true
611
+ # Optional locally; generated from CONNECTORS_MCP_PORT when empty:
612
+ GOOGLE_OAUTH_CALLBACK_URL=
613
+ ```
614
+
615
+ For a local installation, `wiki-workspace agents up` fills an empty callback
616
+ with:
617
+
618
+ ```text
619
+ http://127.0.0.1:<CONNECTORS_MCP_PORT>/oauth/google/callback
620
+ ```
621
+
622
+ With the default port, register this exact redirect URI in Google Cloud
623
+ Console:
624
+
625
+ ```text
626
+ http://127.0.0.1:3338/oauth/google/callback
627
+ ```
628
+
629
+ The browser resolves `127.0.0.1`; Docker forwards the published host port to
630
+ the connectors container. For a remote deployment, set an explicit public
631
+ HTTPS callback instead. In both cases the configured URL must match the Google
632
+ Cloud redirect URI exactly.
633
+
634
+ The local flow uses the public wikiLLM Desktop OAuth Client ID with PKCE and
635
+ does not require a Client Secret. Normal users set neither Google credential.
636
+ `GOOGLE_OAUTH_CLIENT_ID` remains an advanced override for private/internal
637
+ Google projects, and `GOOGLE_OAUTH_CLIENT_SECRET` is an optional compatibility
638
+ override for administrators using a confidential web client.
639
+
640
+ `agents up` also generates the connectors MCP token plus distinct OAuth
641
+ start/state secrets when missing. It adds a regular, standard MCP `connectors`
642
+ entry to `mcp.endpoints.json`. Setting `CONNECTORS_ENABLED=false` and running
643
+ `agents up` removes that entry again, so disabled services are not probed. No
644
+ non-standard `enabled` property is written to MCP configuration files.
645
+ The matching `chatAccess.connectors` policy is managed at the same time. Its
646
+ read-only `allow` list exposes `connectors_google_status`, while the explicit
647
+ `allowActions` list exposes only `connectors_google_oauth_start`. Orchestration
648
+ tools such as `agent_execute` are never exposed directly to served chat.
649
+
650
+ Connector authorization is also available without asking the LLM. These two
651
+ commands work in both the Shell UI and the `llm-wiki serve` chat:
652
+
653
+ ```text
654
+ /connector list
655
+ /connector auth google
656
+ ```
657
+
658
+ The first reports the Gmail read-only authorization state for the active
659
+ workspace. The second opens Google's OAuth page in the browser. Asking Donna
660
+ to configure or check Google remains supported through the direct connector
661
+ tools above.
662
+
663
+ The Compose profile is an internal implementation detail. Do not set
664
+ `COMPOSE_PROFILES` and do not add provider names such as Gmail or Slack to it:
665
+ one `agent-connectors` service hosts all connector providers.
666
+
667
+ For a public serve deployment, authorization can be started through the
668
+ same-origin proxy:
589
669
 
590
670
  ```bash
591
- cp "$(npm root -g)/@dotdrelle/wiki-manager/agents.docker-compose.mailer.example.yml" \
592
- agents.docker-compose.override.yml
671
+ curl -X POST https://wiki.example.com/api/connectors/google/oauth/start \
672
+ -H 'Origin: https://wiki.example.com' \
673
+ -H 'X-LLM-WIKI-OAUTH: 1' \
674
+ -H 'Content-Type: application/json' \
675
+ -d '{"instanceId":"google-1"}'
676
+ ```
677
+
678
+ Open the returned `authorizationUrl`. The workspace is injected by serve and
679
+ cannot be selected by the browser request.
680
+
681
+ Donna discovers the agent contract automatically from the `connectors` MCP
682
+ endpoint. With only one provider, no routing entry is required. To pin it
683
+ explicitly in a workspace profile:
684
+
685
+ ```yaml
686
+ capabilityRouting:
687
+ external-source.collect:
688
+ preferredAgents: [connectors]
689
+ allowedAgents: [connectors]
593
690
  ```
594
691
 
692
+ #### Optional agents and user overrides
693
+
694
+ Anything beyond the packaged stack is an external connector operated and
695
+ configured independently by the user. To run one alongside the packaged
696
+ agents, create a file named
697
+ `agents.docker-compose.override.yml` **next to your `.env`**:
698
+
595
699
  `agents up` includes it automatically when present (standard Docker Compose
596
700
  merge: new services are added, same-name keys override the defaults — you can
597
701
  also use it to pin a port or a variable of a default agent). The file is
@@ -599,7 +703,8 @@ yours: wiki-manager never generates or overwrites it. Complete the setup by
599
703
  adding the connector's variables to your `.env` and its endpoint block to
600
704
  your `mcp.endpoints.json` — every variable an external MCP endpoint needs
601
705
  lives in the `.env` and is referenced as `${VAR_NAME}` from
602
- `mcp.endpoints.json`. Detailed steps are in the example file header.
706
+ `mcp.endpoints.json`. A connector running outside the manager Compose stack
707
+ only needs an entry in `mcp.endpoints.json`.
603
708
 
604
709
  Workspace-native MCP servers (`llm-wiki`, `production`) stay configured through
605
710
  each workspace `.env`. External agents are workspace-agnostic: the active
@@ -1010,7 +1115,7 @@ llm-wiki-manager/
1010
1115
  │ ├── useAgent.ts # agent call wrapper (drives the @langchain/langgraph run)
1011
1116
  │ └── renderer.ts # markdown stripping and line coloring
1012
1117
  ├── docker-compose.yml # workspace-scoped stack (serve, mcp-http, production-mcp)
1013
- ├── agents.docker-compose.yml # global external agents (cme, documents, mailer)
1118
+ ├── agents.docker-compose.yml # packaged global external agents
1014
1119
  ├── wiki-workspace
1015
1120
  ├── .env.example # template for local .env (WORKSPACES_ROOT, agent tokens, …)
1016
1121
  ├── mcp.endpoints.example.json
@@ -1,6 +1,8 @@
1
1
  # agents.docker-compose.yml — external agent stack
2
2
  #
3
3
  # Starts cme and documents as workspace-agnostic global services.
4
+ # The connectors service is opt-in with CONNECTORS_ENABLED=true; wiki-workspace
5
+ # translates that setting to the internal Compose profile.
4
6
  # All workspace paths are resolved at tool-call time via the `workspace` param.
5
7
  #
6
8
  # Required:
@@ -12,6 +14,11 @@
12
14
  # DOCUMENTS_MCP_PORT — 3337
13
15
  # CME_MCP_AUTH_TOKEN — bearer token for cme agent (empty = no auth)
14
16
  # DOCUMENTS_MCP_AUTH_TOKEN — bearer token for documents agent (empty = no auth)
17
+ # CONNECTORS_MCP_PORT — 3338
18
+ # CONNECTORS_MCP_AUTH_TOKEN — bearer token for connectors MCP
19
+ # GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET — OAuth application
20
+ # GOOGLE_OAUTH_CALLBACK_URL — exact public post-proxy callback URL
21
+ # OAUTH_STATE_SECRET / OAUTH_START_TOKEN — distinct 32+ byte secrets
15
22
  # DOCUMENT_LLM_BASE_URL — https://albert.api.etalab.gouv.fr/v1
16
23
  # DOCUMENT_LLM_MODEL — lightonai/LightOnOCR-2-1B
17
24
  # DOCUMENT_LLM_API_KEY — OpenAI/OpenAI-compatible API key for document OCR
@@ -76,3 +83,37 @@ services:
76
83
  - ${WORKSPACES_ROOT:?Set WORKSPACES_ROOT to the directory containing all workspace folders}:/workspaces
77
84
  #- ${AGENTS_DATA_DIR:-./.agents-data}/certs:/certs:ro
78
85
  restart: unless-stopped
86
+
87
+ connectors:
88
+ profiles: [connectors]
89
+ # Local iteration: build the image from the connectors repo instead of
90
+ # pulling the published tag. `image:` is kept so the build is tagged with the
91
+ # same name; restore image-only before publishing a release.
92
+ build:
93
+ context: ../agent-external/agent-connectors
94
+ dockerfile: Dockerfile
95
+ image: dotdrelle/agent-connectors:latest
96
+ user: "${UID:-1000}:${GID:-1000}"
97
+ ports:
98
+ - "${CONNECTORS_MCP_PORT:-3338}:3338"
99
+ environment:
100
+ - CONNECTORS_PORT=3338
101
+ - MCP_AUTH_TOKEN=${CONNECTORS_MCP_AUTH_TOKEN:-}
102
+ - WORKSPACES_ROOT=/workspaces
103
+ - AGENT_DATA_DIR=/data
104
+ - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-}
105
+ - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-}
106
+ - GOOGLE_OAUTH_CALLBACK_URL=${GOOGLE_OAUTH_CALLBACK_URL:-}
107
+ - OAUTH_STATE_SECRET=${OAUTH_STATE_SECRET:-}
108
+ - OAUTH_START_TOKEN=${OAUTH_START_TOKEN:-}
109
+ - OAUTH_STATE_TTL_SECONDS=${OAUTH_STATE_TTL_SECONDS:-600}
110
+ - CONNECTORS_RECOMMENDED_CONCURRENCY=${CONNECTORS_RECOMMENDED_CONCURRENCY:-2}
111
+ - CONNECTORS_MAX_CONCURRENCY=${CONNECTORS_MAX_CONCURRENCY:-4}
112
+ - NODE_USE_ENV_PROXY=${NODE_USE_ENV_PROXY:-}
113
+ - HTTPS_PROXY=${HTTPS_PROXY:-}
114
+ - HTTP_PROXY=${HTTP_PROXY:-}
115
+ - NO_PROXY=${NO_PROXY:-localhost,127.0.0.1,host.docker.internal}
116
+ volumes:
117
+ - ${AGENTS_DATA_DIR:-./.agents-data}/connectors:/data
118
+ - ${WORKSPACES_ROOT:?Set WORKSPACES_ROOT to the directory containing all workspace folders}:/workspaces
119
+ restart: unless-stopped
@@ -37,7 +37,10 @@ services:
37
37
  stop_grace_period: 10s
38
38
  volumes:
39
39
  - ${WIKI_WORKSPACE_PATH:-/tmp/llm-wiki-workspace-not-set}:/workspace
40
- - ./mcp.endpoints.json:/mcp.endpoints.json:ro
40
+ # Absolute path to the running instance's endpoints file (set by the
41
+ # manager to managerStateDir/mcp.endpoints.json). Falls back to the
42
+ # package-relative file only when launched without the manager.
43
+ - ${WIKI_MANAGER_MCP_ENDPOINTS_FILE:-./mcp.endpoints.json}:/mcp.endpoints.json:ro
41
44
  - ${AGENTS_DATA_DIR:-./.agents-data}/documents/input:/documents/input
42
45
  - ${AGENTS_DATA_DIR:-./.agents-data}/documents/uploads:/documents/uploads
43
46
  # TLS certificates — uncomment if WIKI_SERVE_TLS_CERT_PATH is set
@@ -53,6 +56,12 @@ services:
53
56
  - DOCUMENTS_MCP_PORT
54
57
  - CME_MCP_AUTH_TOKEN
55
58
  - DOCUMENTS_MCP_AUTH_TOKEN
59
+ # Connectors MCP: the serve panel resolves ${CONNECTORS_MCP_AUTH_TOKEN} in
60
+ # /mcp.endpoints.json from process.env only, so it must be forwarded here
61
+ # (symmetric with cme/documents) or the panel sends an empty Bearer → 401.
62
+ - CONNECTORS_MCP_PORT
63
+ - CONNECTORS_MCP_AUTH_TOKEN
64
+ - CONNECTORS_OAUTH_START_TOKEN=${OAUTH_START_TOKEN:-}
56
65
  - WORKSPACE_NAME
57
66
  - DOCUMENT_INPUT_DIR=/documents/input
58
67
  - DOCUMENT_UPLOADS_DIR=/documents/uploads
@@ -61,6 +70,7 @@ services:
61
70
  - PRODUCTION_MCP_PROXY_URL=http://host.docker.internal:${PRODUCTION_MCP_PORT:-3102}/mcp/
62
71
  - WIKI_MANAGER_RUNTIME_URL=http://host.docker.internal:${WIKI_MANAGER_RUNTIME_PORT:-7788}
63
72
  - WIKI_MANAGER_RUNTIME_TOKEN=${WIKI_MANAGER_RUNTIME_TOKEN:-}
73
+ - CONNECTORS_AGENT_URL=http://host.docker.internal:${CONNECTORS_MCP_PORT:-3338}
64
74
  # HTTPS — set paths inside the container (e.g. /certs/server.crt) and uncomment the volume above
65
75
  #- WIKI_SERVE_TLS_CERT_PATH=/certs/server.crt
66
76
  #- WIKI_SERVE_TLS_KEY_PATH=/certs/server.key
@@ -110,6 +120,11 @@ services:
110
120
  - WIKI_CONFIG_PATH=${WIKI_CONFIG_PATH:-}
111
121
  - PRODUCTION_ALLOWED_STEPS=${PRODUCTION_ALLOWED_STEPS:-doctor,ingest,ingest_plan,ingest_apply,build,export,polish,pipeline}
112
122
  - PRODUCTION_REQUIRE_CONFIRMATION=${PRODUCTION_REQUIRE_CONFIRMATION:-false}
123
+ # Parallelism levers — effective concurrency ≈ recommendedConcurrency.
124
+ # Intermediate defaults (4/8). Low profile 2/4, high profile 8/16.
125
+ # See docs/configuration.md § "Parallelism & throughput".
126
+ - PRODUCTION_RECOMMENDED_CONCURRENCY=${PRODUCTION_RECOMMENDED_CONCURRENCY:-4}
127
+ - PRODUCTION_MAX_CONCURRENCY=${PRODUCTION_MAX_CONCURRENCY:-8}
113
128
  - PRODUCTION_JOBS_DIR=${PRODUCTION_JOBS_DIR:-/workspace/.wiki/production-jobs}
114
129
  - PRODUCTION_LOCKS_DIR=${PRODUCTION_LOCKS_DIR:-/workspace/.wiki/production-jobs/locks}
115
130
  ports:
@@ -29,9 +29,9 @@
29
29
  "chatAccess": {
30
30
  "maxToolIterations": 8,
31
31
  "servers": {
32
- "wiki": { "allow": ["help_list", "help_read", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source"] },
33
- "production": { "allow": ["production_job_status", "production_jobs_list"] },
34
- "cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status"] }
32
+ "llm-wiki": { "allow": ["help_list", "help_read", "wiki_workspace_status", "wiki_list_pages", "wiki_read_page", "wiki_read_pages", "wiki_search_context", "wiki_collect_context", "wiki_read_ingested_source"] },
33
+ "wiki-production": { "allow": ["production_job_status", "production_jobs_list"] },
34
+ "cme": { "allow": ["cme_status", "cme_sources_list", "cme_export_status"] }
35
35
  }
36
36
  }
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdrelle/wiki-manager",
3
- "version": "0.14.16",
3
+ "version": "0.14.23",
4
4
  "description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "author": "dotrelle",
@@ -27,7 +27,6 @@
27
27
  "wiki-workspace",
28
28
  "docker-compose.yml",
29
29
  "agents.docker-compose.yml",
30
- "agents.docker-compose.mailer.example.yml",
31
30
  "mcp.endpoints.example.json",
32
31
  ".env.example",
33
32
  "tsconfig.json",
@@ -93,17 +93,21 @@ function groupLine(group, activities) {
93
93
  // still progressing. Show the live worker as running; surface the group
94
94
  // failure once no work remains. Otherwise its business label was rendered
95
95
  // red even though that exact task was healthy and advancing.
96
+ // Glyphs mirror the Shell PlanPanel so the two never disagree: done is a
97
+ // check (not a cross — "[x]" read as a failure X), failure is a distinct
98
+ // cross, and an approval wait gets its own pause glyph instead of reusing the
99
+ // failure "[!]".
96
100
  if (running.length > 0 || activeActivity) {
97
101
  icon = '[...]';
98
102
  status = activeProgress != null ? `${Math.round(activeProgress)} %` : `${done}/${total}`;
99
103
  } else if (failed) {
100
- icon = '[!]';
104
+ icon = '[✗]';
101
105
  status = 'error';
102
106
  } else if (done === total) {
103
- icon = '[x]';
107
+ icon = '[✓]';
104
108
  status = 'done';
105
109
  } else if (waitingApproval) {
106
- icon = '[!]';
110
+ icon = '[⏸]';
107
111
  status = 'validation';
108
112
  } else if (done > 0) {
109
113
  icon = '[...]';
@@ -5,6 +5,20 @@ import { aggregateActivity } from './activityAggregator.js';
5
5
  import { visibleActivityEvents } from './activityDeduplicator.js';
6
6
  import { calculateWeightedProgress } from './progressCalculator.js';
7
7
 
8
+ test('a group awaiting approval renders a distinct pause glyph, not a failure', () => {
9
+ const aggregated = aggregateActivity({
10
+ plan: [
11
+ { id: 'publish', label: 'Publication', groupId: 'publish', status: 'pending_approval', progressWeight: 1 },
12
+ ],
13
+ activities: [],
14
+ });
15
+ const line = aggregated.lines.find((item) => /publish|Publication/.test(item.label));
16
+ assert.ok(line, 'the awaiting-approval group is present');
17
+ assert.match(line.label, /\[⏸\]/, 'uses the pause glyph');
18
+ assert.doesNotMatch(line.label, /\[!\]|\[✗\]/, 'never reuses the failure glyph');
19
+ assert.equal(line.status, 'validation');
20
+ });
21
+
8
22
  test('activityDeduplicator keeps one visible entry for repeated 2 percent polls', () => {
9
23
  const events = Array.from({ length: 50 }, () => ({
10
24
  type: 'activity_upserted',
@@ -54,7 +68,7 @@ test('aggregateActivity exposes initial synthesis and grouped display lines', ()
54
68
 
55
69
  assert.deepEqual(activity.initialSynthesis, ['120 sources detectees', '6 traitements simultanes recommandes']);
56
70
  assert.equal(activity.progress.percent, 54);
57
- assert.ok(activity.lines.some((line) => /\[x\] collect - done/.test(line.label)));
71
+ assert.ok(activity.lines.some((line) => /\[✓\] collect - done/.test(line.label)));
58
72
  assert.ok(activity.lines.some((line) => /\[\.\.\.\] customer-data\.enrich - 63 %/.test(line.label)));
59
73
  const enrichLine = activity.lines.find((line) => /customer-data\.enrich/.test(line.label));
60
74
  assert.equal(enrichLine.progress.label, 'Export rapport.md');
@@ -92,7 +106,7 @@ test('aggregateActivity keeps activities not attached to any plan task visible',
92
106
 
93
107
  const aggregated = aggregateActivity(state, []);
94
108
  const labels = aggregated.lines.map((line) => line.label).join('\n');
95
- assert.match(labels, /\[x\] .* done/, 'the done plan group stays visible');
109
+ assert.match(labels, /\[✓\] .* done/, 'the done plan group stays visible');
96
110
  assert.match(labels, /Ingest b87acaf6/, 'the unattached running ingest must appear');
97
111
  const ingestLine = aggregated.lines.find((line) => /Ingest/.test(line.label));
98
112
  assert.equal(ingestLine.status, 'running');
@@ -562,7 +562,8 @@ function assertAgentReadSlashCommandAllowed(commandLine) {
562
562
  function withActiveWorkspaceForExternalTool(session, server, tool, args) {
563
563
  const needsWorkspace =
564
564
  (server === 'documents' && tool.startsWith('documents_') && tool !== 'documents_status') ||
565
- (server === 'cme' && tool.startsWith('cme_') && tool !== 'cme_export_cancel' && !(tool === 'cme_export_status' && args.job_id));
565
+ (server === 'cme' && tool.startsWith('cme_') && tool !== 'cme_export_cancel' && !(tool === 'cme_export_status' && args.job_id)) ||
566
+ (server === 'connectors' && tool.startsWith('connectors_'));
566
567
  if (!needsWorkspace) return args;
567
568
  if (!session.workspace) {
568
569
  throw new Error(`No active workspace available for ${server}.${tool}. Use /use <workspace> first.`);
@@ -820,7 +821,16 @@ function connectorConfigurationTarget(session, objective) {
820
821
  if (!/(?:configur|connect|authent|oauth|setup|sign[ -]?in)/i.test(text)) return null;
821
822
  for (const [serverName, server] of Object.entries(session?.mcp ?? {})) {
822
823
  if (server?.status !== 'connected' || !Array.isArray(server.tools) || server.tools.length === 0) continue;
823
- const aliases = String(serverName).toLowerCase().split(/[^a-z0-9]+/).filter((part) => part.length >= 3);
824
+ const genericAliasParts = new Set([
825
+ 'auth', 'authenticate', 'connect', 'connector', 'connectors',
826
+ 'configure', 'oauth', 'setup', 'start', 'status',
827
+ ]);
828
+ const aliases = [
829
+ serverName,
830
+ ...server.tools.map((tool) => tool?.name ?? ''),
831
+ ]
832
+ .flatMap((value) => String(value).toLowerCase().split(/[^a-z0-9]+/))
833
+ .filter((part) => part.length >= 3 && !genericAliasParts.has(part));
824
834
  if (!aliases.some((alias) => text.includes(alias))) continue;
825
835
  const setupTool = server.tools.find((tool) => {
826
836
  const name = String(tool?.name ?? '').toLowerCase();
@@ -953,20 +963,20 @@ export function buildAgentSystemPrompt(state) {
953
963
  `Current wikirc profile: ${wikirc}.`,
954
964
  `Available primitives: ${commandList(state.session)}.`,
955
965
  'Only announce or call slash commands that appear exactly in Available primitives. Do not invent command names, subcommands, or arguments.',
956
- 'Connected MCP tools you may call directly (server__tool naming convention) — reads AND single-step actions like configuring or adding a connector source, converting a document, sending, or searching. Only the heavy multi-step operations (ingest, build, export, polish, pipeline) go through runtime__delegate to get their parallel plan. Everything listed below is directly callable:',
966
+ 'Connected MCP tools you may call directly (server__tool naming convention). Everything listed below is directly callable. When the requested action has no matching direct tool, call runtime__delegate with the original objective: the runtime resolves it against the discovered agent capability contracts, including executor-only single-task capabilities.',
957
967
  mcpTools,
958
968
  'Current local MCP job queue:',
959
969
  formatQueue(state.session),
960
970
  'Available skills:',
961
971
  skills,
962
- 'In interactive agent mode you may call only the read-only tools and runtime control/delegation tools actually provided to you.',
972
+ 'In interactive agent mode, call only tools actually provided to you. Any directly offered tool stays direct; never substitute an orchestration-contract tool yourself.',
963
973
  'When the user asks for an action that can be performed with connected MCP tools or safe primitives, do not answer with future intent such as "I will call...", "I am going to run...", or "launching..." unless you also call the tool in the same turn. Either call the tool now, ask for the exact missing required arguments, or explain the concrete blocker.',
964
974
  'Execution truthfulness: never invent a job id, status, percentage, duration, generated file, file content, URL, command, or tool result. An action is executed only when you call an available tool and receive its result. Examples and placeholders are forbidden in execution reports.',
965
975
  'After any completed action, give a short factual summary based only on the tool result: outcome and concrete outputs or references actually returned. Mention a viewing primitive only when it exists in Available primitives and is relevant. Never invent results, interpret generated content beyond what the tool returned, or fabricate a verification checklist.',
966
976
  'You are in AGENT mode, so you can actually act. You MAY close with ONE short, natural follow-up — a single sentence phrased as an offer, and only when it genuinely helps and is an action you can perform right here (delegate it or call a tool), e.g. "Want me to start ingesting these pages?" (phrased in the reply language). This is what makes you feel like an assistant rather than a readout. Only offer what you can truly do in agent mode — never an offer that would require another mode. Keep it to that one line: never produce a "Next steps"/"Prochaines étapes"/"À suivre" list, a checklist, an options menu, or commands for the user to type. If nothing useful naturally follows, simply stop after the answer — do not pad.',
967
977
  'When calling a tool, emit no preliminary narration. Call it directly; the PLAN and Activity panels show progress. After completion, keep the final response concise and proportional to the result.',
968
978
  'Write the way a thoughtful colleague speaks: warm, plain, and to the point. For a simple factual question, 1 to 3 sentences is the sweet spot. Stay synthetic and information-dense — use only the lines needed, and never exceed roughly 15 to 20 short lines even for a detailed answer. Never expose internal reasoning, repeated checks, tool-selection commentary, or a chronological diary. Prioritize the result, essential facts, concrete errors, and actual outputs — but say them in human language, not as a field dump.',
969
- 'Only the heavy multi-step operations — ingest, build, export, polish, pipeline — are delegated via runtime__delegate (for their DAG and parallelism). Single-step actions — configuring or adding a connector source, converting a document, sending, searching — are called directly on the connected tool. Never call an agent orchestration-contract or plan tool directly.',
979
+ 'Call a matching direct tool when one is offered. Otherwise, for an action backed by a discovered agent capability, call runtime__delegate with the original objective. This applies to both planner agents and executor-only single-task agents. Never call an agent orchestration-contract or plan tool directly.',
970
980
  'For any question about the current workspace inventory or what is waiting there, call wiki__wiki_workspace_status first and answer only from its result. This is the canonical read-only workspace state; do not reconstruct it from upload, connector, or production tools.',
971
981
  'Tool identifiers are private implementation details. Never print MCP tool names such as server__tool in a user-facing answer. Describe the human result instead.',
972
982
  'Internal data shapes are private too. Never quote raw JSON field names (e.g. pendingSources.files), internal directory paths (e.g. raw/untracked/), or config keys in a user-facing answer — translate them into plain language. Say "36 pages sources sont en attente d\'ingestion", not the field or path they came from.',
@@ -975,10 +985,10 @@ export function buildAgentSystemPrompt(state) {
975
985
  'For service actions, recommend only available service primitives from Available primitives, with the exact service name when the primitive supports one.',
976
986
  'Scope discipline: execute ONLY the action(s) the user explicitly requested. Never chain additional mutating operations (ingest, build, export, polish, delete, send…) that the user did not ask for — even when diagnostics or recommendations suggest them. Finish with the requested result and stop. Example: "applique les recommandations de config" means apply the config; it does NOT authorize launching the ingest those recommendations mention.',
977
987
  state.session.runtime?.url
978
- ? 'The runtime is connected and runtime__delegate is bound and available for heavy orchestrated operations (ingest, build, export, polish, pipeline). Single-step connector actions such as configure, authenticate, add a source, convert, search, or send use the connected MCP tool directly. Never delegate connector configuration to an export capability.'
988
+ ? 'The runtime is connected and runtime__delegate is available for any requested capability action that has no matching direct tool. When a matching direct tool is offered, call it directly; otherwise let the runtime resolve the objective from discovered capability contracts.'
979
989
  : 'No runtime is connected, so you cannot execute actions. State that plainly and name the runtime connection as the missing capability — do not invent a workaround.',
980
990
  'If the connector or service needed for a requested read or action is absent from the Connected MCP tools above (its service is not running — e.g. CME, documents, or production), say plainly that this service is not connected and name it as the missing capability. Never redirect a simple read (e.g. "give me the CME config") to an "agent action", never invent its result, and never propose a workaround. Only requests you can actually serve with a listed tool are answered with data.',
981
- 'For heavy orchestrated operations only (ingest, build, export, polish, pipeline), call runtime__delegate with the user objective only. For a single-step connector action, call the offered connector tool directly. Never choose a capability, operation, agent, plan, or implementation yourself. Never call <provider>__agent_plan, <provider>__agent_execute, legacy production__production_start_job, wiki__plan_set, or wiki__plan_done from interactive chat.',
991
+ 'For an action with no matching direct tool, call runtime__delegate with the user objective only. The runtime chooses the capability, operation, agent and plan, including a validated single task for executor-only agents. Never choose those identifiers yourself. Never call <provider>__agent_plan, <provider>__agent_execute, legacy production__production_start_job, wiki__plan_set, or wiki__plan_done from interactive chat.',
982
992
  'Do not ask the user which sources, files, connectors, or templates to use for an ingest, build, or export: the specialized agent discovers them from the workspace. When the objective is clear (e.g. "lance une ingestion"), delegate it as stated, without a clarifying question.',
983
993
  'Promise only what the resolved capability actually exposes in its declared contract (the input schema the specialized agent publishes for that capability). When the user requests an execution parameter — a batch or chunk size, a count "N at a time", concurrency, ordering, priority, or any tuning knob — apply it only if that parameter exists in the target capability\'s published input schema. Otherwise do not confirm or promise it: delegate the objective, and if the user explicitly asked for that parameter, say plainly in one line that you started the work but do not control that aspect (the runtime and the specialized agent decide it). Never state or imply a parameter was applied when the agent contract cannot enforce it.',
984
994
  'If runtime__delegate returns a blocker or no specialized provider is available, report only that concrete blocker concisely. Never replace the missing execution path with a suggested slash command, skill, MCP tool name, manual file move, administrator escalation, or alternative workflow unless the user explicitly asks for alternatives.',
@@ -1087,10 +1097,48 @@ function isReadOnlyMcpCall(session, server, tool) {
1087
1097
  .find((item) => String(item?.name ?? '') === tool || String(item?.name ?? '').endsWith(`__${tool}`));
1088
1098
  return isDonnaReadTool({
1089
1099
  function: { name: `${server}__${tool}` },
1090
- readOnly: descriptor?.readOnly === true,
1100
+ readOnly: descriptor?.annotations?.readOnlyHint === true || descriptor?.readOnly === true,
1091
1101
  });
1092
1102
  }
1093
1103
 
1104
+ function hasExecutedActionTool(messages, session) {
1105
+ for (const message of messages ?? []) {
1106
+ for (const call of message?.tool_calls ?? []) {
1107
+ const resolved = resolveToolCallName(session?.mcp, call?.function?.name ?? '', INTERNAL_TOOL_SERVERS);
1108
+ const { server, tool } = resolved;
1109
+ if (!server) continue;
1110
+ if (server === 'runtime') {
1111
+ if (tool !== 'status') return true;
1112
+ continue;
1113
+ }
1114
+ if (server === 'shell') {
1115
+ if (tool !== 'read_command') return true;
1116
+ continue;
1117
+ }
1118
+ if (server === 'wiki' && (tool === 'plan_set' || tool === 'plan_done')) return true;
1119
+ if (!isReadOnlyMcpCall(session, server, tool)) return true;
1120
+ }
1121
+ }
1122
+ return false;
1123
+ }
1124
+
1125
+ function hasExecutedReadOnlyTool(messages, session) {
1126
+ for (const message of messages ?? []) {
1127
+ for (const call of message?.tool_calls ?? []) {
1128
+ const { server, tool } = resolveToolCallName(
1129
+ session?.mcp,
1130
+ call?.function?.name ?? '',
1131
+ INTERNAL_TOOL_SERVERS,
1132
+ );
1133
+ if (server === 'runtime' && tool === 'status') return true;
1134
+ if (server === 'shell' && tool === 'read_command') return true;
1135
+ if (server && !['runtime', 'shell'].includes(server)
1136
+ && isReadOnlyMcpCall(session, server, tool)) return true;
1137
+ }
1138
+ }
1139
+ return false;
1140
+ }
1141
+
1094
1142
  export function createAgentGraph(options = {}) {
1095
1143
  async function orchestratorNode(state) {
1096
1144
  const llm = state.session.llm ?? options.llm ?? null;
@@ -1216,9 +1264,17 @@ export function createAgentGraph(options = {}) {
1216
1264
  emitAgentEvent(state.session, 'assistant_message', 'llm', { content: '' });
1217
1265
  state.session._onStep?.(`[${iterations + 1}/${MAX_TOOL_ITERATIONS}] ${result.tool_calls.length} MCP action${result.tool_calls.length > 1 ? 's' : ''} queued…`);
1218
1266
  // On iteration 0 persist the user message too so it survives the loop.
1267
+ // Some OpenAI-compatible providers return tool_calls only at the
1268
+ // top-level result and omit them from `message`. Preserve the calls in
1269
+ // the transcript so follow-up guards can distinguish a status check
1270
+ // from an action that was actually executed.
1271
+ const assistantToolMessage = {
1272
+ ...(result.message ?? { role: 'assistant', content: result.content ?? null }),
1273
+ tool_calls: result.tool_calls,
1274
+ };
1219
1275
  const newMessages = iterations === 0
1220
- ? [{ role: 'user', content: state.input }, result.message]
1221
- : [result.message];
1276
+ ? [{ role: 'user', content: state.input }, assistantToolMessage]
1277
+ : [assistantToolMessage];
1222
1278
  return {
1223
1279
  pendingToolCalls: result.tool_calls,
1224
1280
  allowedToolNames: tools.map((item) => item?.function?.name).filter(Boolean),
@@ -1240,7 +1296,7 @@ export function createAgentGraph(options = {}) {
1240
1296
  result.message ?? { role: 'assistant', content: result.content ?? '' },
1241
1297
  {
1242
1298
  role: 'user',
1243
- content: 'Your previous response did not execute the requested action because it called no tool. Do not narrate or simulate execution. Call the appropriate available tool now. Never invent results.',
1299
+ content: 'Your previous response did not execute the requested action because it called no tool. Do not narrate or simulate execution. Call the matching direct tool now; if no matching direct tool is offered, call runtime__delegate with the original objective. Never invent results.',
1244
1300
  },
1245
1301
  ],
1246
1302
  toolIterations: 1,
@@ -1251,6 +1307,28 @@ export function createAgentGraph(options = {}) {
1251
1307
  }
1252
1308
 
1253
1309
  const canDelegate = tools.some((item) => item?.function?.name === 'runtime__delegate');
1310
+ if (iterations > 0 && canDelegate && !state.forceDelegation
1311
+ && hasExecutedReadOnlyTool(conversationMessages, state.session)
1312
+ && !hasExecutedActionTool(conversationMessages, state.session)
1313
+ && await classifyRequestedAction(llm, state.input, state.session._abortSignal)) {
1314
+ state.session._onStreamReset?.();
1315
+ state.session._onStep?.('Agent: read-only checks completed but requested action not executed — delegating…');
1316
+ return {
1317
+ pendingToolCalls: null,
1318
+ messages: [
1319
+ result.message ?? { role: 'assistant', content: result.content ?? '' },
1320
+ {
1321
+ role: 'user',
1322
+ content: 'You only performed read-only/status checks and did not execute the requested action. Call runtime__delegate now with the original objective only.',
1323
+ },
1324
+ ],
1325
+ toolIterations: iterations + 1,
1326
+ readyToStream: false,
1327
+ inputClassification: classification,
1328
+ retryWithoutTool: true,
1329
+ forceDelegation: true,
1330
+ };
1331
+ }
1254
1332
  if (!runtimeExecution && iterations === 0 && canDelegate && !state.retryWithoutTool
1255
1333
  && await classifyRequestedAction(llm, state.input, state.session._abortSignal)) {
1256
1334
  state.session._onStreamReset?.();
@@ -1570,6 +1648,7 @@ export function createAgentGraph(options = {}) {
1570
1648
  messages: toolResultMessages,
1571
1649
  pendingToolCalls: null,
1572
1650
  forceDelegation: false,
1651
+ retryWithoutTool: false,
1573
1652
  terminalToolFailure: true,
1574
1653
  invalidToolCallRetries: 0,
1575
1654
  invalidResponseRetries: 0,
@@ -1579,6 +1658,7 @@ export function createAgentGraph(options = {}) {
1579
1658
  messages: toolResultMessages,
1580
1659
  pendingToolCalls: null,
1581
1660
  forceDelegation: false,
1661
+ retryWithoutTool: false,
1582
1662
  invalidToolCallRetries: 0,
1583
1663
  invalidResponseRetries: 0,
1584
1664
  terminalToolFailure: false,