maqam 0.1.2 → 0.1.4
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/README.md +27 -5
- package/app/assets/maqam-cli-agent-flow.png +0 -0
- package/docs/usage.md +358 -3
- package/package.json +3 -1
- package/src/framework/agent-tool.js +43 -0
- package/src/framework/cli-agent-tool.js +185 -0
- package/src/index.js +2 -0
package/README.md
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It combines a local agent runtime, policy engine, evidence ledger, skill registry, tool gateway, human-review-ready approval errors, and a crawler-backed research workflow.
|
|
5
|
+
Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It combines a local agent runtime, policy engine, evidence ledger, skill registry, tool gateway, generic agent adapter, human-review-ready approval errors, and a crawler-backed research workflow.
|
|
6
6
|
|
|
7
|
-
The crawler is
|
|
7
|
+
The crawler is not the product center; it is only the first built-in connector. Maqam can govern any agent or tool you register through `ToolGateway`, including function agents, object agents with `run`/`invoke`/`call`, browser agents, research agents, internal SaaS connectors, and write-action agents that need human approval.
|
|
8
8
|
|
|
9
9
|
Full documentation: [docs/usage.md](https://github.com/AjnasNB/maqam/blob/main/docs/usage.md)
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
+

|
|
14
|
+
|
|
13
15
|
## What Ships
|
|
14
16
|
|
|
15
17
|
- `AgentRuntime`: sequential workflow execution with retries, trace events, task outputs, and policy preflight.
|
|
16
18
|
- `PolicyEngine`: deterministic goal and tool-call decisions for allowed tools, origins, limits, and approval gates.
|
|
17
19
|
- `EvidenceLedger`: provenance records, claim links, source hashes, confidence, and unsupported-claim checks.
|
|
18
20
|
- `ToolGateway`: one governed path for external tool execution.
|
|
21
|
+
- `createAgentTool`: wraps any function agent or object agent so Maqam can control it through policy, trace, approval, and evidence.
|
|
22
|
+
- `createCliAgentTool`: wraps fixed command-line workers with timeout, approximate input-token limits, output byte limits, and no shell execution by default.
|
|
19
23
|
- `SkillRegistry`: lightweight skill metadata registration and selection.
|
|
20
24
|
- `createResearchWorkflow`: crawler-backed source collection, synthesis, and quality checks.
|
|
21
25
|
- `maqam`: local web console for running governed research workflows.
|
|
@@ -80,18 +84,32 @@ import {
|
|
|
80
84
|
EvidenceLedger,
|
|
81
85
|
PolicyEngine,
|
|
82
86
|
ToolGateway,
|
|
87
|
+
createAgentTool,
|
|
88
|
+
createCliAgentTool,
|
|
83
89
|
createCrawlerTool,
|
|
84
90
|
createResearchWorkflow
|
|
85
91
|
} from "maqam";
|
|
86
92
|
|
|
87
93
|
const evidenceLedger = new EvidenceLedger();
|
|
88
94
|
const policyEngine = new PolicyEngine({
|
|
89
|
-
allowedTools: ["crawler"],
|
|
95
|
+
allowedTools: ["crawler", "summarizer"],
|
|
90
96
|
allowedOrigins: ["https://github.com", "https://www.npmjs.com"]
|
|
91
97
|
});
|
|
92
98
|
|
|
93
99
|
const gateway = new ToolGateway({ policyEngine, evidenceLedger });
|
|
94
100
|
gateway.registerTool("crawler", createCrawlerTool());
|
|
101
|
+
gateway.registerTool("summarizer", createAgentTool(async (input) => ({
|
|
102
|
+
summary: `Reviewed ${input.topic}`
|
|
103
|
+
}), { name: "summarizer" }));
|
|
104
|
+
gateway.registerTool("localWorker", createCliAgentTool({
|
|
105
|
+
name: "localWorker",
|
|
106
|
+
command: process.execPath,
|
|
107
|
+
args: ["--version"],
|
|
108
|
+
stdin: "none",
|
|
109
|
+
timeoutMs: 5000,
|
|
110
|
+
maxInputTokens: 20,
|
|
111
|
+
maxOutputBytes: 2048
|
|
112
|
+
}));
|
|
95
113
|
|
|
96
114
|
const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway: gateway });
|
|
97
115
|
const result = await runtime.runWorkflow(
|
|
@@ -101,7 +119,7 @@ const result = await runtime.runWorkflow(
|
|
|
101
119
|
}),
|
|
102
120
|
{
|
|
103
121
|
objective: "Research permissive OSS agent framework projects",
|
|
104
|
-
allowedTools: ["crawler"],
|
|
122
|
+
allowedTools: ["crawler", "summarizer"],
|
|
105
123
|
allowedOrigins: ["https://github.com"]
|
|
106
124
|
}
|
|
107
125
|
);
|
|
@@ -149,7 +167,7 @@ Brand assets live in `app/assets/`, including `maqam-logo.svg` and `maqam-brand-
|
|
|
149
167
|
- Use a clear user agent.
|
|
150
168
|
- Rate-limit per origin.
|
|
151
169
|
- Avoid bypassing access controls, paywalls, anti-bot systems, or private content.
|
|
152
|
-
- No required
|
|
170
|
+
- No required model provider dependency.
|
|
153
171
|
- No required external hosted service.
|
|
154
172
|
- Produce JSON/JSONL output that agents can consume directly.
|
|
155
173
|
|
|
@@ -176,3 +194,7 @@ Publishing requires an authenticated npm session with permission to publish the
|
|
|
176
194
|
## License
|
|
177
195
|
|
|
178
196
|
MIT
|
|
197
|
+
|
|
198
|
+
## Open Development
|
|
199
|
+
|
|
200
|
+
Maqam is open source under MIT and open for development, issues, ideas, and contributions.
|
|
Binary file
|
package/docs/usage.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Maqam Usage Guide
|
|
2
2
|
|
|
3
|
-
Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It gives you a small local runtime for building agent systems that can be inspected, policy-checked, and connected to evidence.
|
|
3
|
+
Maqam is an MIT-licensed Ajnas agent framework for governed workflows. It gives you a small local runtime for building agent systems that can be inspected, policy-checked, and connected to evidence. The crawler is only one built-in connector; Maqam can also govern arbitrary agents and tools through `createAgentTool` and `ToolGateway`.
|
|
4
4
|
|
|
5
5
|
This guide covers installation, CLI usage, SDK usage, the local console, crawler usage, API reference, common patterns, and troubleshooting.
|
|
6
6
|
|
|
@@ -14,6 +14,8 @@ This guide covers installation, CLI usage, SDK usage, the local console, crawler
|
|
|
14
14
|
- [Architecture](#architecture)
|
|
15
15
|
- [API Reference](#api-reference)
|
|
16
16
|
- [Build A Custom Workflow](#build-a-custom-workflow)
|
|
17
|
+
- [Control Any Agent](#control-any-agent)
|
|
18
|
+
- [Control CLI Workers](#control-cli-workers)
|
|
17
19
|
- [Register A Custom Tool](#register-a-custom-tool)
|
|
18
20
|
- [Use Policy And Approvals](#use-policy-and-approvals)
|
|
19
21
|
- [Use Evidence And Claims](#use-evidence-and-claims)
|
|
@@ -77,18 +79,32 @@ import {
|
|
|
77
79
|
EvidenceLedger,
|
|
78
80
|
PolicyEngine,
|
|
79
81
|
ToolGateway,
|
|
82
|
+
createAgentTool,
|
|
83
|
+
createCliAgentTool,
|
|
80
84
|
createCrawlerTool,
|
|
81
85
|
createResearchWorkflow
|
|
82
86
|
} from "maqam";
|
|
83
87
|
|
|
84
88
|
const evidenceLedger = new EvidenceLedger();
|
|
85
89
|
const policyEngine = new PolicyEngine({
|
|
86
|
-
allowedTools: ["crawler"],
|
|
90
|
+
allowedTools: ["crawler", "summarizer"],
|
|
87
91
|
allowedOrigins: ["https://github.com"]
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
|
|
91
95
|
toolGateway.registerTool("crawler", createCrawlerTool());
|
|
96
|
+
toolGateway.registerTool("summarizer", createAgentTool(async (input) => ({
|
|
97
|
+
summary: `Reviewed ${input.topic}`
|
|
98
|
+
}), { name: "summarizer" }));
|
|
99
|
+
toolGateway.registerTool("localWorker", createCliAgentTool({
|
|
100
|
+
name: "localWorker",
|
|
101
|
+
command: process.execPath,
|
|
102
|
+
args: ["--version"],
|
|
103
|
+
stdin: "none",
|
|
104
|
+
timeoutMs: 5000,
|
|
105
|
+
maxInputTokens: 20,
|
|
106
|
+
maxOutputBytes: 2048
|
|
107
|
+
}));
|
|
92
108
|
|
|
93
109
|
const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
|
|
94
110
|
const result = await runtime.runWorkflow(
|
|
@@ -98,7 +114,7 @@ const result = await runtime.runWorkflow(
|
|
|
98
114
|
}),
|
|
99
115
|
{
|
|
100
116
|
objective: "Research Maqam from public sources",
|
|
101
|
-
allowedTools: ["crawler"],
|
|
117
|
+
allowedTools: ["crawler", "summarizer"],
|
|
102
118
|
allowedOrigins: ["https://github.com"],
|
|
103
119
|
budget: { maxToolCalls: 20, maxRuntimeMs: 120_000 }
|
|
104
120
|
}
|
|
@@ -210,6 +226,8 @@ import {
|
|
|
210
226
|
PolicyEngine,
|
|
211
227
|
ToolGateway,
|
|
212
228
|
SkillRegistry,
|
|
229
|
+
createAgentTool,
|
|
230
|
+
createCliAgentTool,
|
|
213
231
|
createCrawlerTool,
|
|
214
232
|
createResearchWorkflow,
|
|
215
233
|
crawl,
|
|
@@ -245,6 +263,8 @@ Core objects:
|
|
|
245
263
|
- `AgentRuntime`: owns workflow execution.
|
|
246
264
|
- `PolicyEngine`: decides what is allowed, denied, or approval-gated.
|
|
247
265
|
- `ToolGateway`: routes all external tool calls through policy.
|
|
266
|
+
- `createAgentTool`: wraps arbitrary agents so they can be governed like any other tool.
|
|
267
|
+
- `createCliAgentTool`: wraps fixed command-line workers with timeout, approximate input-token limits, output byte limits, and no shell execution by default.
|
|
248
268
|
- `EvidenceLedger`: stores source evidence and claim support.
|
|
249
269
|
- `SkillRegistry`: stores skill metadata and selects matching skills.
|
|
250
270
|
- `createResearchWorkflow`: bundled workflow for public web research.
|
|
@@ -560,6 +580,69 @@ await toolGateway.call("crawler", {
|
|
|
560
580
|
});
|
|
561
581
|
```
|
|
562
582
|
|
|
583
|
+
### `createAgentTool(agent, options)`
|
|
584
|
+
|
|
585
|
+
Wraps an arbitrary agent so it can be controlled by Maqam policy and executed through `ToolGateway`.
|
|
586
|
+
|
|
587
|
+
Supported agent shapes:
|
|
588
|
+
|
|
589
|
+
- Function agent: `async (input, context) => output`
|
|
590
|
+
- Object agent with `run(input, context)`
|
|
591
|
+
- Object agent with `invoke(input, context)`
|
|
592
|
+
- Object agent with `call(input, context)`
|
|
593
|
+
|
|
594
|
+
```js
|
|
595
|
+
const summarizer = createAgentTool(async (input, context) => {
|
|
596
|
+
return {
|
|
597
|
+
summary: `Reviewed ${input.topic}`,
|
|
598
|
+
evidence: [
|
|
599
|
+
{
|
|
600
|
+
evidenceId: "ev_agent_1",
|
|
601
|
+
sourceType: "agent_output",
|
|
602
|
+
source: "summarizer",
|
|
603
|
+
excerpt: "The agent reviewed policy and evidence controls.",
|
|
604
|
+
confidence: 0.8
|
|
605
|
+
}
|
|
606
|
+
],
|
|
607
|
+
claims: [
|
|
608
|
+
{
|
|
609
|
+
text: "The summarizer reviewed policy and evidence controls.",
|
|
610
|
+
evidenceIds: ["ev_agent_1"],
|
|
611
|
+
confidence: 0.8
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
};
|
|
615
|
+
}, { name: "summarizer" });
|
|
616
|
+
|
|
617
|
+
toolGateway.registerTool("summarizer", summarizer);
|
|
618
|
+
|
|
619
|
+
const result = await toolGateway.call("summarizer", {
|
|
620
|
+
topic: "Maqam"
|
|
621
|
+
}, {
|
|
622
|
+
runId: "run_1",
|
|
623
|
+
taskId: "summarize"
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
If the agent output includes `evidence` or `claims` arrays, Maqam records them into the active `EvidenceLedger`.
|
|
628
|
+
|
|
629
|
+
Object-agent example:
|
|
630
|
+
|
|
631
|
+
```js
|
|
632
|
+
const browserAgent = {
|
|
633
|
+
async run(input) {
|
|
634
|
+
return {
|
|
635
|
+
url: input.url,
|
|
636
|
+
result: "Browser task completed"
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
toolGateway.registerTool("browserAgent", createAgentTool(browserAgent, {
|
|
642
|
+
name: "browserAgent"
|
|
643
|
+
}));
|
|
644
|
+
```
|
|
645
|
+
|
|
563
646
|
### `createResearchWorkflow(options)`
|
|
564
647
|
|
|
565
648
|
Creates the bundled public research workflow.
|
|
@@ -573,6 +656,93 @@ const workflow = createResearchWorkflow({
|
|
|
573
656
|
});
|
|
574
657
|
```
|
|
575
658
|
|
|
659
|
+
### `createCliAgentTool(options)`
|
|
660
|
+
|
|
661
|
+
Wraps a fixed command-line worker so it can run through Maqam policy and trace capture.
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
const localWorker = createCliAgentTool({
|
|
665
|
+
name: "localWorker",
|
|
666
|
+
command: process.execPath,
|
|
667
|
+
args: ["--input-type=module", "-e", "let body=''; for await (const c of process.stdin) body += c; const input = JSON.parse(body); console.log(JSON.stringify({ artifact: `built:${input.name}` }));"],
|
|
668
|
+
stdin: "json",
|
|
669
|
+
parseJson: true,
|
|
670
|
+
timeoutMs: 5000,
|
|
671
|
+
maxInputTokens: 50,
|
|
672
|
+
maxOutputBytes: 2048
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
toolGateway.registerTool("localWorker", localWorker);
|
|
676
|
+
|
|
677
|
+
const result = await toolGateway.call("localWorker", {
|
|
678
|
+
name: "demo-widget"
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
console.log(result.json.artifact);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Options:
|
|
685
|
+
|
|
686
|
+
| Field | Type | Description |
|
|
687
|
+
| --- | --- | --- |
|
|
688
|
+
| `name` | `string` | Name used in result metadata. |
|
|
689
|
+
| `command` | `string` | Fixed executable path or command. Required. |
|
|
690
|
+
| `args` | `string[]` | Fixed argument list. Dynamic user input should go through stdin, not command args. |
|
|
691
|
+
| `cwd` | `string` | Optional working directory. |
|
|
692
|
+
| `env` | `object` | Extra environment variables. |
|
|
693
|
+
| `inheritEnv` | `boolean` | Inherit `process.env`. Default: `true`. |
|
|
694
|
+
| `stdin` | `"json" | "text" | "none"` | How input is passed to the worker. Default: `"json"`. |
|
|
695
|
+
| `parseJson` | `boolean` | Parse stdout as JSON and expose it as `result.json`. |
|
|
696
|
+
| `timeoutMs` | `number` | Hard runtime timeout. Default: `30000`. |
|
|
697
|
+
| `maxInputTokens` | `number` | Approximate input token limit. Default: `4000`. |
|
|
698
|
+
| `maxOutputBytes` | `number` | Maximum combined stdout/stderr bytes. Default: `65536`. |
|
|
699
|
+
| `rejectOnNonZero` | `boolean` | Reject when exit code is not zero. Default: `true`. |
|
|
700
|
+
| `shell` | `boolean` | Run through a shell. Default: `false`. Use only when a platform wrapper requires it. |
|
|
701
|
+
|
|
702
|
+
Result shape:
|
|
703
|
+
|
|
704
|
+
```json
|
|
705
|
+
{
|
|
706
|
+
"name": "localWorker",
|
|
707
|
+
"command": "node",
|
|
708
|
+
"args": ["--version"],
|
|
709
|
+
"exitCode": 0,
|
|
710
|
+
"signal": null,
|
|
711
|
+
"timedOut": false,
|
|
712
|
+
"stdout": "v20.0.0\n",
|
|
713
|
+
"stderr": "",
|
|
714
|
+
"durationMs": 42,
|
|
715
|
+
"approxInputTokens": 0,
|
|
716
|
+
"outputBytes": 9,
|
|
717
|
+
"limits": {
|
|
718
|
+
"maxInputTokens": 50,
|
|
719
|
+
"maxOutputBytes": 2048,
|
|
720
|
+
"timeoutMs": 5000
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Limit errors:
|
|
726
|
+
|
|
727
|
+
| Code | Meaning |
|
|
728
|
+
| --- | --- |
|
|
729
|
+
| `CLI_INPUT_LIMIT_EXCEEDED` | Input was too large before execution. |
|
|
730
|
+
| `CLI_OUTPUT_LIMIT_EXCEEDED` | stdout/stderr exceeded the configured byte limit. |
|
|
731
|
+
| `CLI_TIMEOUT` | Process exceeded `timeoutMs`. |
|
|
732
|
+
| `CLI_EXIT_NONZERO` | Process exited with a non-zero code. |
|
|
733
|
+
| `CLI_JSON_PARSE_FAILED` | `parseJson` was enabled but stdout was not valid JSON. |
|
|
734
|
+
| `CLI_SPAWN_FAILED` | The process could not be started. |
|
|
735
|
+
|
|
736
|
+
Security notes:
|
|
737
|
+
|
|
738
|
+
- Maqam does not use a shell for CLI workers by default.
|
|
739
|
+
- Keep `command` and `args` fixed in code.
|
|
740
|
+
- Send user input through stdin.
|
|
741
|
+
- Use narrow `allowedTools`.
|
|
742
|
+
- Set short `timeoutMs` and small `maxOutputBytes` for untrusted workers.
|
|
743
|
+
- Use approval gates for workers that write, publish, send, or modify state.
|
|
744
|
+
- Prefer direct executable paths over platform wrapper scripts. On Windows, some `.cmd` or `.ps1` shims may require `shell: true` or a direct underlying executable path.
|
|
745
|
+
|
|
576
746
|
Tasks:
|
|
577
747
|
|
|
578
748
|
| Task ID | Purpose |
|
|
@@ -741,6 +911,187 @@ console.log(result.outputs.record_summary);
|
|
|
741
911
|
console.log(result.evidence.unsupportedClaims);
|
|
742
912
|
```
|
|
743
913
|
|
|
914
|
+
## Control Any Agent
|
|
915
|
+
|
|
916
|
+
Yes, Maqam can control agents beyond crawling. The pattern is:
|
|
917
|
+
|
|
918
|
+
1. Wrap the agent with `createAgentTool`.
|
|
919
|
+
2. Register it in `ToolGateway`.
|
|
920
|
+
3. Put the agent name in `PolicyEngine.allowedTools`.
|
|
921
|
+
4. Add it to `approvalRequiredTools` if it can write, publish, send, modify, or spend.
|
|
922
|
+
5. Call it from an `AgentRuntime` workflow task.
|
|
923
|
+
|
|
924
|
+
Example with multiple agents:
|
|
925
|
+
|
|
926
|
+
```js
|
|
927
|
+
import {
|
|
928
|
+
AgentRuntime,
|
|
929
|
+
EvidenceLedger,
|
|
930
|
+
PolicyEngine,
|
|
931
|
+
ToolGateway,
|
|
932
|
+
createAgentTool
|
|
933
|
+
} from "maqam";
|
|
934
|
+
|
|
935
|
+
const evidenceLedger = new EvidenceLedger();
|
|
936
|
+
const policyEngine = new PolicyEngine({
|
|
937
|
+
allowedTools: ["researchAgent", "reviewAgent", "publishAgent"],
|
|
938
|
+
approvalRequiredTools: ["publishAgent"]
|
|
939
|
+
});
|
|
940
|
+
const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
|
|
941
|
+
|
|
942
|
+
toolGateway.registerTool("researchAgent", createAgentTool(async (input) => ({
|
|
943
|
+
notes: `Researched ${input.topic}`,
|
|
944
|
+
evidence: [
|
|
945
|
+
{
|
|
946
|
+
evidenceId: "ev_research_1",
|
|
947
|
+
sourceType: "agent_output",
|
|
948
|
+
source: "researchAgent",
|
|
949
|
+
excerpt: `Researched ${input.topic}`,
|
|
950
|
+
confidence: 0.7
|
|
951
|
+
}
|
|
952
|
+
]
|
|
953
|
+
}), { name: "researchAgent" }));
|
|
954
|
+
|
|
955
|
+
toolGateway.registerTool("reviewAgent", createAgentTool({
|
|
956
|
+
async run(input) {
|
|
957
|
+
return { approvedForDraft: Boolean(input.notes) };
|
|
958
|
+
}
|
|
959
|
+
}, { name: "reviewAgent" }));
|
|
960
|
+
|
|
961
|
+
toolGateway.registerTool("publishAgent", createAgentTool(async () => ({
|
|
962
|
+
published: true
|
|
963
|
+
}), { name: "publishAgent" }));
|
|
964
|
+
|
|
965
|
+
const workflow = {
|
|
966
|
+
name: "multi_agent_governed_flow",
|
|
967
|
+
tasks: [
|
|
968
|
+
{
|
|
969
|
+
id: "research",
|
|
970
|
+
run: (context) => context.tools.call("researchAgent", { topic: "Maqam" }, context)
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
id: "review",
|
|
974
|
+
run: (context) => context.tools.call("reviewAgent", context.outputs.research, context)
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
id: "publish",
|
|
978
|
+
run: (context) => context.tools.call("publishAgent", context.outputs.review, context)
|
|
979
|
+
}
|
|
980
|
+
]
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
|
|
984
|
+
const result = await runtime.runWorkflow(workflow, {
|
|
985
|
+
objective: "Run a governed multi-agent workflow",
|
|
986
|
+
allowedTools: ["researchAgent", "reviewAgent", "publishAgent"]
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
console.log(result.status);
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
In this example, `publishAgent` will throw `ApprovalRequiredError` because it is approval-gated. That is intentional: Maqam controls the agent rather than letting it publish directly.
|
|
993
|
+
|
|
994
|
+
What Maqam can control:
|
|
995
|
+
|
|
996
|
+
- Function agents.
|
|
997
|
+
- LangChain/LangGraph-style agents if exposed through `invoke` or wrapped in a function.
|
|
998
|
+
- External SDK-style functions if wrapped in a function.
|
|
999
|
+
- Browser agents.
|
|
1000
|
+
- Research agents.
|
|
1001
|
+
- GitHub/npm/internal API agents.
|
|
1002
|
+
- Email, Slack, Jira, database, or release agents when registered as tools.
|
|
1003
|
+
|
|
1004
|
+
What Maqam cannot do automatically:
|
|
1005
|
+
|
|
1006
|
+
- It cannot control an agent you do not route through `ToolGateway`.
|
|
1007
|
+
- It cannot make an unsafe third-party agent safe if that agent bypasses the wrapper and performs side effects internally.
|
|
1008
|
+
- It cannot approve risky actions by itself; approval-gated actions should be routed to humans.
|
|
1009
|
+
|
|
1010
|
+
## Control CLI Workers
|
|
1011
|
+
|
|
1012
|
+
Maqam can govern command-line workers the same way it governs function agents.
|
|
1013
|
+
|
|
1014
|
+
The pattern is:
|
|
1015
|
+
|
|
1016
|
+
1. Create a fixed CLI adapter with `createCliAgentTool`.
|
|
1017
|
+
2. Register it with `ToolGateway`.
|
|
1018
|
+
3. Add the worker name to `allowedTools`.
|
|
1019
|
+
4. Configure `timeoutMs`, `maxInputTokens`, and `maxOutputBytes`.
|
|
1020
|
+
5. Call the worker from a workflow task.
|
|
1021
|
+
|
|
1022
|
+
Example workflow:
|
|
1023
|
+
|
|
1024
|
+
```js
|
|
1025
|
+
import {
|
|
1026
|
+
AgentRuntime,
|
|
1027
|
+
EvidenceLedger,
|
|
1028
|
+
PolicyEngine,
|
|
1029
|
+
ToolGateway,
|
|
1030
|
+
createCliAgentTool
|
|
1031
|
+
} from "maqam";
|
|
1032
|
+
|
|
1033
|
+
const evidenceLedger = new EvidenceLedger();
|
|
1034
|
+
const policyEngine = new PolicyEngine({
|
|
1035
|
+
allowedTools: ["builderWorker"]
|
|
1036
|
+
});
|
|
1037
|
+
const toolGateway = new ToolGateway({ policyEngine, evidenceLedger });
|
|
1038
|
+
|
|
1039
|
+
toolGateway.registerTool("builderWorker", createCliAgentTool({
|
|
1040
|
+
name: "builderWorker",
|
|
1041
|
+
command: process.execPath,
|
|
1042
|
+
args: ["--input-type=module", "-e", "let body=''; for await (const c of process.stdin) body += c; const input = JSON.parse(body); console.log(JSON.stringify({ fileName: `${input.name}.txt`, content: `Built ${input.name}` }));"],
|
|
1043
|
+
stdin: "json",
|
|
1044
|
+
parseJson: true,
|
|
1045
|
+
timeoutMs: 5000,
|
|
1046
|
+
maxInputTokens: 100,
|
|
1047
|
+
maxOutputBytes: 4096
|
|
1048
|
+
}));
|
|
1049
|
+
|
|
1050
|
+
const workflow = {
|
|
1051
|
+
name: "governed_cli_build",
|
|
1052
|
+
tasks: [
|
|
1053
|
+
{
|
|
1054
|
+
id: "build",
|
|
1055
|
+
run: (context) => context.tools.call("builderWorker", {
|
|
1056
|
+
name: "demo-widget"
|
|
1057
|
+
}, context)
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
id: "record",
|
|
1061
|
+
run: (context) => {
|
|
1062
|
+
const output = context.outputs.build.json;
|
|
1063
|
+
const evidence = context.evidence.addEvidence({
|
|
1064
|
+
runId: context.runId,
|
|
1065
|
+
taskId: "record",
|
|
1066
|
+
sourceType: "cli_worker_output",
|
|
1067
|
+
source: "builderWorker",
|
|
1068
|
+
excerpt: output.content,
|
|
1069
|
+
tool: "builderWorker",
|
|
1070
|
+
confidence: 0.8
|
|
1071
|
+
});
|
|
1072
|
+
context.evidence.addClaim({
|
|
1073
|
+
runId: context.runId,
|
|
1074
|
+
taskId: "record",
|
|
1075
|
+
text: `The worker created ${output.fileName}.`,
|
|
1076
|
+
evidenceIds: [evidence.evidenceId],
|
|
1077
|
+
confidence: 0.8
|
|
1078
|
+
});
|
|
1079
|
+
return output;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
]
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
const runtime = new AgentRuntime({ policyEngine, evidenceLedger, toolGateway });
|
|
1086
|
+
const result = await runtime.runWorkflow(workflow, {
|
|
1087
|
+
objective: "Build a small artifact through a governed CLI worker",
|
|
1088
|
+
allowedTools: ["builderWorker"]
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
console.log(result.outputs.record);
|
|
1092
|
+
console.log(result.evidence.unsupportedClaims);
|
|
1093
|
+
```
|
|
1094
|
+
|
|
744
1095
|
## Register A Custom Tool
|
|
745
1096
|
|
|
746
1097
|
Tools should be small and explicit. The gateway handles policy and trace capture.
|
|
@@ -1092,3 +1443,7 @@ Useful next packages or modules:
|
|
|
1092
1443
|
- Browser automation connector.
|
|
1093
1444
|
- GitHub and npm metadata connectors.
|
|
1094
1445
|
- Tenant-aware configuration and audit export.
|
|
1446
|
+
|
|
1447
|
+
## Open Development
|
|
1448
|
+
|
|
1449
|
+
Maqam is open source under MIT and open for development, issues, ideas, and contributions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maqam",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Maqam is an MIT-licensed Ajnas agent framework for governed workflows, policy, evidence, skills, and crawler-backed research.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"skills",
|
|
44
44
|
"tool-orchestration",
|
|
45
45
|
"human-approval",
|
|
46
|
+
"cli-agent",
|
|
47
|
+
"command-runner",
|
|
46
48
|
"crawler",
|
|
47
49
|
"agent",
|
|
48
50
|
"web-crawler",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function resolveAgentInvoker(agent) {
|
|
2
|
+
if (typeof agent === "function") return agent;
|
|
3
|
+
if (agent && typeof agent.run === "function") return agent.run.bind(agent);
|
|
4
|
+
if (agent && typeof agent.invoke === "function") return agent.invoke.bind(agent);
|
|
5
|
+
if (agent && typeof agent.call === "function") return agent.call.bind(agent);
|
|
6
|
+
throw new TypeError("createAgentTool requires a function agent or an object with run, invoke, or call.");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function recordAgentEvidence(result, context, agentName) {
|
|
10
|
+
const ledger = context.evidenceLedger || context.evidence;
|
|
11
|
+
if (!ledger || !result || typeof result !== "object") return;
|
|
12
|
+
|
|
13
|
+
for (const item of result.evidence || []) {
|
|
14
|
+
ledger.addEvidence({
|
|
15
|
+
runId: context.runId || item.runId || null,
|
|
16
|
+
taskId: context.taskId || item.taskId || null,
|
|
17
|
+
tool: context.toolName || agentName,
|
|
18
|
+
...item
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const item of result.claims || []) {
|
|
23
|
+
ledger.addClaim({
|
|
24
|
+
runId: context.runId || item.runId || null,
|
|
25
|
+
taskId: context.taskId || item.taskId || null,
|
|
26
|
+
...item
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createAgentTool(agent, options = {}) {
|
|
32
|
+
const invoke = resolveAgentInvoker(agent);
|
|
33
|
+
const agentName = options.name || agent?.name || "agent";
|
|
34
|
+
|
|
35
|
+
return async function agentTool(input = {}, context = {}) {
|
|
36
|
+
const result = await invoke(input, {
|
|
37
|
+
...context,
|
|
38
|
+
agentName
|
|
39
|
+
});
|
|
40
|
+
recordAgentEvidence(result, context, agentName);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { AjnasFrameworkError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
|
+
const DEFAULT_MAX_INPUT_TOKENS = 4_000;
|
|
6
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
7
|
+
|
|
8
|
+
function estimateTokens(value) {
|
|
9
|
+
return Math.ceil(Buffer.byteLength(value || "", "utf8") / 4);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildStdin(input, mode) {
|
|
13
|
+
if (mode === "none") return null;
|
|
14
|
+
if (mode === "text") return String(input.prompt ?? input.text ?? "");
|
|
15
|
+
return JSON.stringify(input);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cliError(message, code, details = {}) {
|
|
19
|
+
return new AjnasFrameworkError(message, {
|
|
20
|
+
code,
|
|
21
|
+
details
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createCliAgentTool(options = {}) {
|
|
26
|
+
const {
|
|
27
|
+
name = "cliAgent",
|
|
28
|
+
command,
|
|
29
|
+
args = [],
|
|
30
|
+
cwd,
|
|
31
|
+
env = {},
|
|
32
|
+
inheritEnv = true,
|
|
33
|
+
stdin = "json",
|
|
34
|
+
parseJson = false,
|
|
35
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
36
|
+
maxInputTokens = DEFAULT_MAX_INPUT_TOKENS,
|
|
37
|
+
maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
|
|
38
|
+
rejectOnNonZero = true,
|
|
39
|
+
shell = false
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
if (!command || typeof command !== "string") {
|
|
43
|
+
throw new TypeError("createCliAgentTool requires a fixed command string.");
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) {
|
|
46
|
+
throw new TypeError("createCliAgentTool args must be an array of strings.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return async function cliAgentTool(input = {}) {
|
|
50
|
+
const stdinBody = buildStdin(input, stdin);
|
|
51
|
+
const approxInputTokens = estimateTokens(stdinBody || "");
|
|
52
|
+
if (maxInputTokens && approxInputTokens > maxInputTokens) {
|
|
53
|
+
throw cliError(`CLI input exceeds maxInputTokens (${approxInputTokens} > ${maxInputTokens}).`, "CLI_INPUT_LIMIT_EXCEEDED", {
|
|
54
|
+
name,
|
|
55
|
+
approxInputTokens,
|
|
56
|
+
maxInputTokens
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
let child;
|
|
63
|
+
try {
|
|
64
|
+
child = spawn(command, args, {
|
|
65
|
+
cwd,
|
|
66
|
+
env: inheritEnv ? { ...process.env, ...env } : { ...env },
|
|
67
|
+
shell,
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
reject(cliError(error.message, "CLI_SPAWN_FAILED", {
|
|
73
|
+
name,
|
|
74
|
+
command,
|
|
75
|
+
cause: error.code || error.name
|
|
76
|
+
}));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stdout = [];
|
|
81
|
+
const stderr = [];
|
|
82
|
+
let outputBytes = 0;
|
|
83
|
+
let settled = false;
|
|
84
|
+
|
|
85
|
+
const finish = (callback, value) => {
|
|
86
|
+
if (settled) return;
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
callback(value);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const stopWithError = (error) => {
|
|
93
|
+
if (!child.killed) child.kill();
|
|
94
|
+
finish(reject, error);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
stopWithError(cliError(`CLI agent '${name}' timed out after ${timeoutMs}ms.`, "CLI_TIMEOUT", {
|
|
99
|
+
name,
|
|
100
|
+
timeoutMs
|
|
101
|
+
}));
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
|
|
104
|
+
const collect = (target, chunk) => {
|
|
105
|
+
outputBytes += chunk.byteLength;
|
|
106
|
+
if (outputBytes > maxOutputBytes) {
|
|
107
|
+
stopWithError(cliError(`CLI output exceeds maxOutputBytes (${outputBytes} > ${maxOutputBytes}).`, "CLI_OUTPUT_LIMIT_EXCEEDED", {
|
|
108
|
+
name,
|
|
109
|
+
maxOutputBytes,
|
|
110
|
+
outputBytes
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
target.push(Buffer.from(chunk));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
child.stdout.on("data", (chunk) => collect(stdout, chunk));
|
|
118
|
+
child.stderr.on("data", (chunk) => collect(stderr, chunk));
|
|
119
|
+
|
|
120
|
+
child.on("error", (error) => {
|
|
121
|
+
finish(reject, cliError(error.message, "CLI_SPAWN_FAILED", {
|
|
122
|
+
name,
|
|
123
|
+
command,
|
|
124
|
+
cause: error.code || error.name
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
child.on("close", (exitCode, signal) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
|
|
131
|
+
const stdoutText = Buffer.concat(stdout).toString("utf8");
|
|
132
|
+
const stderrText = Buffer.concat(stderr).toString("utf8");
|
|
133
|
+
const result = {
|
|
134
|
+
name,
|
|
135
|
+
command,
|
|
136
|
+
args,
|
|
137
|
+
exitCode,
|
|
138
|
+
signal,
|
|
139
|
+
timedOut: false,
|
|
140
|
+
stdout: stdoutText,
|
|
141
|
+
stderr: stderrText,
|
|
142
|
+
durationMs: Date.now() - startedAt,
|
|
143
|
+
approxInputTokens,
|
|
144
|
+
outputBytes,
|
|
145
|
+
limits: {
|
|
146
|
+
maxInputTokens,
|
|
147
|
+
maxOutputBytes,
|
|
148
|
+
timeoutMs
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (parseJson && stdoutText.trim()) {
|
|
153
|
+
try {
|
|
154
|
+
result.json = JSON.parse(stdoutText.trim());
|
|
155
|
+
} catch (error) {
|
|
156
|
+
finish(reject, cliError("CLI stdout was not valid JSON.", "CLI_JSON_PARSE_FAILED", {
|
|
157
|
+
name,
|
|
158
|
+
message: error.message
|
|
159
|
+
}));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (rejectOnNonZero && exitCode !== 0) {
|
|
165
|
+
finish(reject, cliError(`CLI agent '${name}' exited with code ${exitCode}.`, "CLI_EXIT_NONZERO", {
|
|
166
|
+
...result,
|
|
167
|
+
stdout: stdoutText.slice(0, 2048),
|
|
168
|
+
stderr: stderrText.slice(0, 2048)
|
|
169
|
+
}));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
finish(resolve, result);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (stdinBody === null) {
|
|
177
|
+
child.stdin.end();
|
|
178
|
+
} else {
|
|
179
|
+
child.stdin.end(stdinBody);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { estimateTokens as estimateCliInputTokens };
|
package/src/index.js
CHANGED
|
@@ -339,6 +339,8 @@ export { EvidenceLedger } from "./framework/evidence-ledger.js";
|
|
|
339
339
|
export { ToolGateway } from "./framework/tool-gateway.js";
|
|
340
340
|
export { SkillRegistry } from "./framework/skill-registry.js";
|
|
341
341
|
export { AgentRuntime } from "./framework/runtime.js";
|
|
342
|
+
export { createAgentTool } from "./framework/agent-tool.js";
|
|
343
|
+
export { createCliAgentTool, estimateCliInputTokens } from "./framework/cli-agent-tool.js";
|
|
342
344
|
export { createResearchWorkflow } from "./framework/research-workflow.js";
|
|
343
345
|
|
|
344
346
|
export function createCrawlerTool(defaultOptions = {}) {
|