@rse/ase 1.0.7 → 2.0.0
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/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +228 -111
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as v from "valibot";
|
|
7
|
+
import * as Core from "./ase-task-store-core.js";
|
|
8
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
9
|
+
/* the maximum number of recent processing log lines kept per agent (by the relay and its
|
|
10
|
+
subscribers), the maximum number of lines per announcement (as an agent keeps its not yet
|
|
11
|
+
announced lines while the relay is unreachable), and the duration after which an agent
|
|
12
|
+
without heartbeat counts as stopped */
|
|
13
|
+
export const RECENT_MAX = 100;
|
|
14
|
+
export const LINES_MAX = 100;
|
|
15
|
+
export const STALE_MS = 30 * 1000;
|
|
16
|
+
/* the maximum length of a processing log line message kept by the relay (as
|
|
17
|
+
the agents themselves already truncate their messages to this length) */
|
|
18
|
+
export const MSG_MAX = 500;
|
|
19
|
+
/* the maximum number of projects, of agents per project, and of agents in total
|
|
20
|
+
held by the relay, and the maximum number of subscribers per project and in total,
|
|
21
|
+
bounding the memory of the relay against misbehaving or malicious clients */
|
|
22
|
+
export const PROJECTS_MAX = 128;
|
|
23
|
+
export const AGENTS_MAX = 64;
|
|
24
|
+
export const AGENTS_TOTAL_MAX = 1024;
|
|
25
|
+
export const SUBS_MAX = 64;
|
|
26
|
+
export const SUBS_TOTAL_MAX = 1024;
|
|
27
|
+
/* append processing log lines to the recent ones, keeping the most recent ones only */
|
|
28
|
+
export const appendLines = (recent, lines) => lines.length === 0 ? recent : [...recent, ...lines].slice(-RECENT_MAX);
|
|
29
|
+
/* add two usages, where a field stays null only if it is null in both */
|
|
30
|
+
export const addUsage = (a, b) => {
|
|
31
|
+
const add = (x, y) => x === null && y === null ? null : (x ?? 0) + (y ?? 0);
|
|
32
|
+
return {
|
|
33
|
+
walltimeSec: a.walltimeSec + b.walltimeSec,
|
|
34
|
+
costToken: add(a.costToken, b.costToken),
|
|
35
|
+
costUSD: add(a.costUSD, b.costUSD)
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/* the valid agent ids (as derived from host, process id, and agent name) */
|
|
39
|
+
export const AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
40
|
+
/* the valibot schema entries of a processing log line and of an announcement */
|
|
41
|
+
const text = (max) => v.pipe(v.string(), v.maxLength(max));
|
|
42
|
+
const lineEntries = {
|
|
43
|
+
time: v.number(),
|
|
44
|
+
level: v.picklist(["error", "warning", "info"]),
|
|
45
|
+
task: text(255),
|
|
46
|
+
msg: text(2000)
|
|
47
|
+
};
|
|
48
|
+
const status = v.pipe(text(64), v.minLength(1));
|
|
49
|
+
const flowEntries = {
|
|
50
|
+
input: v.pipe(v.array(status), v.maxLength(32)),
|
|
51
|
+
activity: v.optional(status),
|
|
52
|
+
output: v.pipe(v.array(status), v.maxLength(32)),
|
|
53
|
+
gate: v.optional(status)
|
|
54
|
+
};
|
|
55
|
+
const amount = v.pipe(v.number(), v.minValue(0));
|
|
56
|
+
const usageEntries = {
|
|
57
|
+
walltimeSec: amount,
|
|
58
|
+
costToken: v.nullable(amount),
|
|
59
|
+
costUSD: v.nullable(amount)
|
|
60
|
+
};
|
|
61
|
+
const announceEntries = {
|
|
62
|
+
kind: v.pipe(text(64), v.minLength(1)),
|
|
63
|
+
name: v.pipe(text(64), v.minLength(1)),
|
|
64
|
+
mode: v.optional(text(64), ""),
|
|
65
|
+
title: v.optional(text(80)),
|
|
66
|
+
description: v.optional(text(600)),
|
|
67
|
+
request: v.optional(text(64)),
|
|
68
|
+
prompt: v.optional(v.picklist(["optional", "ignored"])),
|
|
69
|
+
host: text(255),
|
|
70
|
+
pid: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
71
|
+
cwd: v.optional(text(4096)),
|
|
72
|
+
user: v.optional(text(255)),
|
|
73
|
+
uid: v.optional(v.pipe(v.number(), v.integer(), v.minValue(-1))),
|
|
74
|
+
state: v.picklist(["waiting", "busy"]),
|
|
75
|
+
tasks: v.pipe(v.array(v.pipe(text(255), v.regex(TaskFormat.TASK_ID_RE))), v.maxLength(64))
|
|
76
|
+
};
|
|
77
|
+
/* the valibot schema of an announcement (strict, as received by the relay) */
|
|
78
|
+
const announceSchema = v.strictObject({
|
|
79
|
+
...announceEntries,
|
|
80
|
+
states: v.optional(v.strictObject(flowEntries)),
|
|
81
|
+
started: v.optional(v.number()),
|
|
82
|
+
usage: v.optional(v.nullable(v.strictObject({
|
|
83
|
+
total: v.strictObject(usageEntries),
|
|
84
|
+
last: v.strictObject(usageEntries)
|
|
85
|
+
})), null),
|
|
86
|
+
lines: v.pipe(v.array(v.strictObject(lineEntries)), v.maxLength(LINES_MAX))
|
|
87
|
+
});
|
|
88
|
+
/* the valibot schema of an event frame (lenient on additional fields,
|
|
89
|
+
as received by the subscribers from a potentially newer relay) */
|
|
90
|
+
export const frameSchema = v.object({
|
|
91
|
+
reset: v.optional(v.boolean()),
|
|
92
|
+
agents: v.optional(v.record(v.pipe(v.string(), v.regex(AGENT_ID_RE)), v.object({
|
|
93
|
+
...announceEntries,
|
|
94
|
+
states: v.optional(v.object(flowEntries)),
|
|
95
|
+
id: v.pipe(v.string(), v.regex(AGENT_ID_RE)),
|
|
96
|
+
started: v.number(),
|
|
97
|
+
usage: v.optional(v.nullable(v.object({
|
|
98
|
+
total: v.object(usageEntries),
|
|
99
|
+
last: v.object(usageEntries)
|
|
100
|
+
})), null),
|
|
101
|
+
since: v.number(),
|
|
102
|
+
beat: v.number(),
|
|
103
|
+
recent: v.pipe(v.array(v.object(lineEntries)), v.maxLength(RECENT_MAX))
|
|
104
|
+
}))),
|
|
105
|
+
removed: v.optional(v.array(v.string()))
|
|
106
|
+
});
|
|
107
|
+
/* the transport-agnostic agent relay: the presence of the agents per project (with
|
|
108
|
+
a project existing implicitly while it has agents), the removal of the agents
|
|
109
|
+
without heartbeat, and the emitting of an event frame on every change */
|
|
110
|
+
export class AgentRelayCore {
|
|
111
|
+
emit;
|
|
112
|
+
projects = new Map();
|
|
113
|
+
timer = null;
|
|
114
|
+
constructor(emit) {
|
|
115
|
+
this.emit = emit;
|
|
116
|
+
}
|
|
117
|
+
/* start and stop the periodic removal of the stale agents */
|
|
118
|
+
start() {
|
|
119
|
+
this.timer = setInterval(() => { this.sweep(); }, 5 * 1000);
|
|
120
|
+
}
|
|
121
|
+
stop() {
|
|
122
|
+
if (this.timer !== null)
|
|
123
|
+
clearInterval(this.timer);
|
|
124
|
+
this.timer = null;
|
|
125
|
+
this.projects.clear();
|
|
126
|
+
}
|
|
127
|
+
/* validate a project id and an agent id */
|
|
128
|
+
validate(prjId, agentId) {
|
|
129
|
+
if (!TaskFormat.ID_RE.test(prjId))
|
|
130
|
+
throw Core.problem(422, `invalid project id "${prjId}" (expected: [A-Za-z0-9_-]+)`);
|
|
131
|
+
if (agentId !== undefined && !AGENT_ID_RE.test(agentId))
|
|
132
|
+
throw Core.problem(422, `invalid agent id "${agentId}" (expected: [A-Za-z0-9][A-Za-z0-9_.-]*)`);
|
|
133
|
+
}
|
|
134
|
+
/* the total number of agents of all projects */
|
|
135
|
+
agentsTotal() {
|
|
136
|
+
let total = 0;
|
|
137
|
+
for (const agents of this.projects.values())
|
|
138
|
+
total += agents.size;
|
|
139
|
+
return total;
|
|
140
|
+
}
|
|
141
|
+
/* the projects with agents, each with its number of agents */
|
|
142
|
+
projectList() {
|
|
143
|
+
return [...this.projects.entries()].map(([id, agents]) => ({ id, agents: agents.size }));
|
|
144
|
+
}
|
|
145
|
+
/* the agents of a project */
|
|
146
|
+
list(prjId) {
|
|
147
|
+
this.validate(prjId);
|
|
148
|
+
return [...(this.projects.get(prjId)?.values() ?? [])];
|
|
149
|
+
}
|
|
150
|
+
/* announce the presence of an agent (also as its heartbeat), keeping the time since
|
|
151
|
+
which it is in its state while its state is unchanged, appending its added processing
|
|
152
|
+
log lines to its recent ones, and emitting only its added lines (as a delta), where an
|
|
153
|
+
agent not announcing its start time (an older one) counts as started at its first announcement */
|
|
154
|
+
announce(prjId, agentId, payload) {
|
|
155
|
+
this.validate(prjId, agentId);
|
|
156
|
+
const result = v.safeParse(announceSchema, payload);
|
|
157
|
+
if (!result.success)
|
|
158
|
+
throw Core.problem(422, `invalid agent announcement: ${result.issues[0].message}` +
|
|
159
|
+
(result.issues[0].path !== undefined ? ` (at "${v.getDotPath(result.issues[0]) ?? ""}")` : ""));
|
|
160
|
+
/* reject a new agent (or a new project) beyond the maximum numbers */
|
|
161
|
+
let agents = this.projects.get(prjId);
|
|
162
|
+
const previous = agents?.get(agentId);
|
|
163
|
+
if (previous === undefined) {
|
|
164
|
+
if (agents === undefined && this.projects.size >= PROJECTS_MAX)
|
|
165
|
+
throw Core.problem(429, `too many projects with agents (maximum: ${PROJECTS_MAX})`);
|
|
166
|
+
if (agents !== undefined && agents.size >= AGENTS_MAX)
|
|
167
|
+
throw Core.problem(429, `too many agents of project "${prjId}" (maximum: ${AGENTS_MAX})`);
|
|
168
|
+
if (this.agentsTotal() >= AGENTS_TOTAL_MAX)
|
|
169
|
+
throw Core.problem(429, `too many agents in total (maximum: ${AGENTS_TOTAL_MAX})`);
|
|
170
|
+
}
|
|
171
|
+
if (agents === undefined) {
|
|
172
|
+
agents = new Map();
|
|
173
|
+
this.projects.set(prjId, agents);
|
|
174
|
+
}
|
|
175
|
+
/* update the presence, truncating the messages of the added lines */
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const { lines: received, ...announced } = result.output;
|
|
178
|
+
const lines = received.map((line) => line.msg.length <= MSG_MAX ? line :
|
|
179
|
+
{ ...line, msg: `${line.msg.slice(0, MSG_MAX - 1)}…` });
|
|
180
|
+
const presence = {
|
|
181
|
+
...announced, id: agentId,
|
|
182
|
+
started: announced.started ?? previous?.started ?? now,
|
|
183
|
+
since: previous !== undefined && previous.state === announced.state ? previous.since : now,
|
|
184
|
+
beat: now,
|
|
185
|
+
recent: appendLines(previous?.recent ?? [], lines)
|
|
186
|
+
};
|
|
187
|
+
agents.set(agentId, presence);
|
|
188
|
+
this.emit(prjId, { agents: { [agentId]: { ...presence, recent: lines } } });
|
|
189
|
+
}
|
|
190
|
+
/* remove an agent (on its stopping), where a project without agents ceases to exist */
|
|
191
|
+
remove(prjId, agentId) {
|
|
192
|
+
this.validate(prjId, agentId);
|
|
193
|
+
const agents = this.projects.get(prjId);
|
|
194
|
+
if (agents === undefined || !agents.delete(agentId))
|
|
195
|
+
return;
|
|
196
|
+
if (agents.size === 0)
|
|
197
|
+
this.projects.delete(prjId);
|
|
198
|
+
this.emit(prjId, { removed: [agentId] });
|
|
199
|
+
}
|
|
200
|
+
/* remove all agents without a heartbeat for the stale duration */
|
|
201
|
+
sweep() {
|
|
202
|
+
const limit = Date.now() - STALE_MS;
|
|
203
|
+
for (const [prjId, agents] of this.projects) {
|
|
204
|
+
const removed = [...agents.values()].filter((agent) => agent.beat < limit).map((agent) => agent.id);
|
|
205
|
+
for (const id of removed)
|
|
206
|
+
agents.delete(id);
|
|
207
|
+
if (agents.size === 0)
|
|
208
|
+
this.projects.delete(prjId);
|
|
209
|
+
if (removed.length > 0)
|
|
210
|
+
this.emit(prjId, { removed });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import Hapi from "@hapi/hapi";
|
|
7
|
+
import { WebSocketServer } from "ws";
|
|
8
|
+
import * as Core from "./ase-task-store-core.js";
|
|
9
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
10
|
+
import * as Relay from "./ase-agent-relay-core.js";
|
|
11
|
+
import { ServerGuard, urlHost } from "./ase-lib-server-bind.js";
|
|
12
|
+
/* the route path templates and the JSON payload route options (bounding the
|
|
13
|
+
request body to the size of an announcement with all its lines at maximum) */
|
|
14
|
+
const P = "/projects/{prjId}/agents";
|
|
15
|
+
const json = { payload: { parse: true, allow: "application/json", maxBytes: 512 * 1024 } };
|
|
16
|
+
/* the interval of the keepalive pings of the subscribers, the maximum size of
|
|
17
|
+
the messages of the subscribers (which send none at all), and the maximum amount
|
|
18
|
+
of not yet transmitted data of a subscriber (beyond which it cannot keep up) */
|
|
19
|
+
const PING_MS = 15 * 1000;
|
|
20
|
+
const PAYLOAD_MAX = 4 * 1024;
|
|
21
|
+
const BUFFER_MAX = 1024 * 1024;
|
|
22
|
+
/* the agent relay endpoints, mountable onto a Hapi server which authenticates all
|
|
23
|
+
requests and renders the thrown problems (the standalone agent relay server, or the
|
|
24
|
+
task store server co-hosting the agent relay): the REST endpoints of the agents and
|
|
25
|
+
the WebSocket event endpoint of the projects, which the hosting server delegates to */
|
|
26
|
+
export class AgentRelayBinding {
|
|
27
|
+
log;
|
|
28
|
+
core;
|
|
29
|
+
wss = new WebSocketServer({ noServer: true, maxPayload: PAYLOAD_MAX });
|
|
30
|
+
subs = new Map();
|
|
31
|
+
unanswered = new WeakSet();
|
|
32
|
+
timer = null;
|
|
33
|
+
constructor(log) {
|
|
34
|
+
this.log = log;
|
|
35
|
+
this.core = new Relay.AgentRelayCore((prjId, frame) => { this.emit(prjId, frame); });
|
|
36
|
+
}
|
|
37
|
+
/* whether a request path belongs to the agent relay */
|
|
38
|
+
static handles(path) {
|
|
39
|
+
return path === "/agents" || /^\/projects\/[^/]+\/agents(?:\/|$)/.test(path);
|
|
40
|
+
}
|
|
41
|
+
/* start and stop the agent relay and the keepalive pings of the
|
|
42
|
+
subscribers, closing all subscriptions with close code 1001 */
|
|
43
|
+
start() {
|
|
44
|
+
this.core.start();
|
|
45
|
+
this.timer = setInterval(() => { this.keepalive(); }, PING_MS);
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
if (this.timer !== null)
|
|
49
|
+
clearInterval(this.timer);
|
|
50
|
+
this.timer = null;
|
|
51
|
+
for (const subs of this.subs.values())
|
|
52
|
+
for (const ws of subs)
|
|
53
|
+
ws.close(1001, "server shutting down");
|
|
54
|
+
this.subs.clear();
|
|
55
|
+
this.wss.close();
|
|
56
|
+
this.core.stop();
|
|
57
|
+
}
|
|
58
|
+
/* ping all subscribers, terminating those which have not answered the previous
|
|
59
|
+
ping (e.g. on a half-open connection after a network partition or a host crash) */
|
|
60
|
+
keepalive() {
|
|
61
|
+
for (const [prjId, subs] of this.subs)
|
|
62
|
+
for (const ws of subs) {
|
|
63
|
+
if (this.unanswered.has(ws)) {
|
|
64
|
+
this.log.write("debug", `relay: terminating unresponsive subscriber of project "${prjId}"`);
|
|
65
|
+
ws.terminate();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
this.unanswered.add(ws);
|
|
69
|
+
ws.ping();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/* deliver an event frame to the subscribers of a project, terminating those which
|
|
73
|
+
cannot keep up (as a skipped delta frame would corrupt their view of the agents,
|
|
74
|
+
and they will receive a fresh snapshot on their reconnection anyway) */
|
|
75
|
+
emit(prjId, frame) {
|
|
76
|
+
const data = JSON.stringify(frame);
|
|
77
|
+
for (const ws of this.subs.get(prjId) ?? []) {
|
|
78
|
+
if (ws.readyState !== ws.OPEN)
|
|
79
|
+
continue;
|
|
80
|
+
if (ws.bufferedAmount > BUFFER_MAX) {
|
|
81
|
+
this.log.write("debug", `relay: terminating congested subscriber of project "${prjId}"`);
|
|
82
|
+
ws.terminate();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
ws.send(data);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/* handle a (by the hosting server already authenticated) WebSocket handshake on the
|
|
89
|
+
event endpoint of a project, starting the subscription with the snapshot of its agents */
|
|
90
|
+
upgrade(req, socket, head, path, reject) {
|
|
91
|
+
const m = /^\/projects\/([^/]+)\/agents\/events$/.exec(path);
|
|
92
|
+
if (m === null) {
|
|
93
|
+
reject(404, `no WebSocket endpoint "${path}"`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const prjId = m[1];
|
|
97
|
+
if (!TaskFormat.ID_RE.test(prjId)) {
|
|
98
|
+
reject(422, `invalid project id "${prjId}" (expected: [A-Za-z0-9_-]+)`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
/* reject a subscriber beyond the maximum numbers */
|
|
102
|
+
let total = 0;
|
|
103
|
+
for (const subs of this.subs.values())
|
|
104
|
+
total += subs.size;
|
|
105
|
+
if ((this.subs.get(prjId)?.size ?? 0) >= Relay.SUBS_MAX) {
|
|
106
|
+
reject(429, `too many subscribers of project "${prjId}" (maximum: ${Relay.SUBS_MAX})`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (total >= Relay.SUBS_TOTAL_MAX) {
|
|
110
|
+
reject(429, `too many subscribers in total (maximum: ${Relay.SUBS_TOTAL_MAX})`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
114
|
+
let subs = this.subs.get(prjId);
|
|
115
|
+
if (subs === undefined) {
|
|
116
|
+
subs = new Set();
|
|
117
|
+
this.subs.set(prjId, subs);
|
|
118
|
+
}
|
|
119
|
+
subs.add(ws);
|
|
120
|
+
this.log.write("debug", `relay: subscribed to agents of project "${prjId}"`);
|
|
121
|
+
const agents = Object.fromEntries(this.core.list(prjId).map((agent) => [agent.id, agent]));
|
|
122
|
+
ws.send(JSON.stringify({ reset: true, agents }));
|
|
123
|
+
ws.on("pong", () => {
|
|
124
|
+
this.unanswered.delete(ws);
|
|
125
|
+
});
|
|
126
|
+
ws.on("close", () => {
|
|
127
|
+
const subs = this.subs.get(prjId);
|
|
128
|
+
if (subs !== undefined) {
|
|
129
|
+
subs.delete(ws);
|
|
130
|
+
if (subs.size === 0)
|
|
131
|
+
this.subs.delete(prjId);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
ws.on("error", (err) => {
|
|
135
|
+
this.log.write("debug", `relay: WebSocket error on agents of project "${prjId}": ${err.message}`);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/* install the REST endpoints (and the plain HTTP fallback of the event endpoint) */
|
|
140
|
+
install(server) {
|
|
141
|
+
const core = this.core;
|
|
142
|
+
const p = (request) => request.params;
|
|
143
|
+
server.route({
|
|
144
|
+
method: "GET",
|
|
145
|
+
path: "/agents",
|
|
146
|
+
handler: () => ({ projects: core.projectList() })
|
|
147
|
+
});
|
|
148
|
+
server.route({
|
|
149
|
+
method: "GET",
|
|
150
|
+
path: P,
|
|
151
|
+
handler: (request) => ({ agents: core.list(p(request).prjId) })
|
|
152
|
+
});
|
|
153
|
+
server.route({
|
|
154
|
+
method: "PUT",
|
|
155
|
+
path: `${P}/{agentId}`,
|
|
156
|
+
options: json,
|
|
157
|
+
handler: (request, h) => {
|
|
158
|
+
core.announce(p(request).prjId, p(request).agentId, request.payload);
|
|
159
|
+
return h.response().code(204);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
server.route({
|
|
163
|
+
method: "DELETE",
|
|
164
|
+
path: `${P}/{agentId}`,
|
|
165
|
+
handler: (request, h) => {
|
|
166
|
+
core.remove(p(request).prjId, p(request).agentId);
|
|
167
|
+
return h.response().code(204);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
server.route({
|
|
171
|
+
method: "GET",
|
|
172
|
+
path: `${P}/events`,
|
|
173
|
+
handler: () => {
|
|
174
|
+
throw Core.problem(426, "the event endpoint requires a WebSocket handshake");
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/* the standalone agent relay server: bearer token authentication, CORS,
|
|
180
|
+
problem details rendering, and the agent relay endpoints */
|
|
181
|
+
export class AgentRelayServer {
|
|
182
|
+
log;
|
|
183
|
+
options;
|
|
184
|
+
server;
|
|
185
|
+
binding;
|
|
186
|
+
constructor(log, options) {
|
|
187
|
+
this.log = log;
|
|
188
|
+
this.options = options;
|
|
189
|
+
this.binding = new AgentRelayBinding(log);
|
|
190
|
+
this.server = Hapi.server({
|
|
191
|
+
host: options.address,
|
|
192
|
+
port: options.port,
|
|
193
|
+
tls: options.tls ?? undefined,
|
|
194
|
+
routes: {
|
|
195
|
+
cors: options.cors.length === 0 ? false : {
|
|
196
|
+
origin: options.cors,
|
|
197
|
+
headers: ["Authorization", "Content-Type"],
|
|
198
|
+
maxAge: 86400
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
/* install the common extensions, logging the requests at debug level
|
|
203
|
+
only (as the agents announce frequently), and the agent relay endpoints */
|
|
204
|
+
const guard = new ServerGuard(log, {
|
|
205
|
+
label: "relay",
|
|
206
|
+
token: options.token,
|
|
207
|
+
failure: "internal agent relay failure (see relay log for details)",
|
|
208
|
+
level: () => "debug",
|
|
209
|
+
upgrade: (req, socket, head) => {
|
|
210
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
211
|
+
const path = url.pathname;
|
|
212
|
+
if (!guard.authorized(req.headers.authorization, url.searchParams.get("token") ?? undefined))
|
|
213
|
+
guard.rejectUpgrade(socket, 401, "missing or invalid bearer token", path);
|
|
214
|
+
else
|
|
215
|
+
this.binding.upgrade(req, socket, head, path, (status, detail) => {
|
|
216
|
+
guard.rejectUpgrade(socket, status, detail, path);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
guard.install(this.server);
|
|
221
|
+
this.binding.install(this.server);
|
|
222
|
+
}
|
|
223
|
+
/* start the server: start the agent relay and bind the listener */
|
|
224
|
+
async start() {
|
|
225
|
+
this.binding.start();
|
|
226
|
+
try {
|
|
227
|
+
await this.server.start();
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
this.binding.stop();
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
const scheme = this.options.tls !== null ? "https" : "http";
|
|
234
|
+
this.log.write("info", `relay: listening on ${scheme}://${urlHost(this.options.address)}:${this.options.port}`);
|
|
235
|
+
}
|
|
236
|
+
/* stop the server: stop the agent relay and unbind the listener */
|
|
237
|
+
async stop() {
|
|
238
|
+
this.binding.stop();
|
|
239
|
+
await this.server.stop({ timeout: 1000 });
|
|
240
|
+
this.log.write("info", "relay: terminated");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import * as v from "valibot";
|
|
7
|
+
import { ServerCommand, configuredTls } from "./ase-lib-server-cli.js";
|
|
8
|
+
import { Config } from "./ase-config-core.js";
|
|
9
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
10
|
+
import * as Server from "./ase-agent-relay-server-bind.js";
|
|
11
|
+
/* schema for the per-user "relay.yaml" */
|
|
12
|
+
export const relaySchema = v.nullish(v.strictObject({
|
|
13
|
+
address: v.optional(v.string()),
|
|
14
|
+
port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(65535))),
|
|
15
|
+
token: v.optional(v.string()),
|
|
16
|
+
pid: v.optional(v.pipe(v.number(), v.integer())),
|
|
17
|
+
cors: v.optional(v.array(v.string())),
|
|
18
|
+
tls: v.optional(v.strictObject({
|
|
19
|
+
cert: v.optional(v.string()),
|
|
20
|
+
key: v.optional(v.string())
|
|
21
|
+
}))
|
|
22
|
+
}));
|
|
23
|
+
/* the configured TLS certificate and key of the agent relay: "project.relay.crt" and
|
|
24
|
+
"project.relay.key" of the configuration <cfg>, unless "project.relay.url" selects
|
|
25
|
+
HTTP on this host (as the agents and task boards following it would then fail) */
|
|
26
|
+
export const relayTls = (cfg) => configuredTls(cfg, "project.relay");
|
|
27
|
+
/* CLI command group "ase agent relay" */
|
|
28
|
+
export default class AgentRelayCommand extends ServerCommand {
|
|
29
|
+
label = "relay";
|
|
30
|
+
description = "Manage the per-user agent relay server, passing the live presence of the agents to the task boards";
|
|
31
|
+
name = "relay";
|
|
32
|
+
schema = relaySchema;
|
|
33
|
+
prefix = "ASE_AGENT_RELAY";
|
|
34
|
+
words = "agent relay";
|
|
35
|
+
probePath = "/agents";
|
|
36
|
+
/* the agent relay has no specific configuration */
|
|
37
|
+
context(ctx) {
|
|
38
|
+
return ctx;
|
|
39
|
+
}
|
|
40
|
+
/* the TLS certificate and key configured in the project configuration */
|
|
41
|
+
tlsDefaults() {
|
|
42
|
+
const cfg = new Config("config", configSchema, this.log);
|
|
43
|
+
cfg.read();
|
|
44
|
+
return relayTls(cfg);
|
|
45
|
+
}
|
|
46
|
+
/* create the standalone agent relay server */
|
|
47
|
+
async create(_ctx, config) {
|
|
48
|
+
return { server: new Server.AgentRelayServer(this.log, config), facts: {} };
|
|
49
|
+
}
|
|
50
|
+
}
|