foundry-task-orchestration-actor 0.1.0__py3-none-any.whl
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.
- foundry_task_orchestration_actor/__init__.py +63 -0
- foundry_task_orchestration_actor/cards/actor-data.yaml +91 -0
- foundry_task_orchestration_actor/cards/actor-message.yaml +32 -0
- foundry_task_orchestration_actor/cards/actor-synchronous-messaging.yaml +38 -0
- foundry_task_orchestration_actor/cards/actor.yaml +22 -0
- foundry_task_orchestration_actor/cli.py +186 -0
- foundry_task_orchestration_actor/config.py +578 -0
- foundry_task_orchestration_actor/conformance.py +136 -0
- foundry_task_orchestration_actor/correlation.py +198 -0
- foundry_task_orchestration_actor/deploy.py +399 -0
- foundry_task_orchestration_actor/handler.py +257 -0
- foundry_task_orchestration_actor/instance.py +134 -0
- foundry_task_orchestration_actor/kubeconfig.py +83 -0
- foundry_task_orchestration_actor/peers.py +91 -0
- foundry_task_orchestration_actor/pulls.py +187 -0
- foundry_task_orchestration_actor/round0.py +187 -0
- foundry_task_orchestration_actor/schemas/agentic-context.schema.yaml +135 -0
- foundry_task_orchestration_actor/serve.py +130 -0
- foundry_task_orchestration_actor/settings.py +124 -0
- foundry_task_orchestration_actor-0.1.0.dist-info/METADATA +230 -0
- foundry_task_orchestration_actor-0.1.0.dist-info/RECORD +23 -0
- foundry_task_orchestration_actor-0.1.0.dist-info/WHEEL +4 -0
- foundry_task_orchestration_actor-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""foundry-task-orchestration-actor — drive one capability's task from agreement to pull request.
|
|
2
|
+
|
|
3
|
+
An actor, for one use, with a `papeete-actor` underneath. It holds a three amigos round between
|
|
4
|
+
the testing and implementation actors, then drives implement → deploy → black-box test → retry,
|
|
5
|
+
and opens the paired pull requests on a pass. It judges nothing: every step is a fixed rule, so its
|
|
6
|
+
one door names no engine. The capability it serves is supplied by a sidecar
|
|
7
|
+
(`actor-agentic-context.yaml`, `foundry-task-orchestration-actor/agentic-context/v1`), never by
|
|
8
|
+
this package.
|
|
9
|
+
|
|
10
|
+
Wiring one up is three lines:
|
|
11
|
+
|
|
12
|
+
from foundry_task_orchestration_actor import CapabilityConfig, make_orchestrate_task
|
|
13
|
+
|
|
14
|
+
config = CapabilityConfig.load(".")
|
|
15
|
+
actor = Actor.from_card(".", mailbox=mailbox,
|
|
16
|
+
actions={"orchestrate-task": make_orchestrate_task(config)})
|
|
17
|
+
|
|
18
|
+
`make_orchestrate_task` takes an optional `Settings` too; without one it reads the environment.
|
|
19
|
+
|
|
20
|
+
Those lines are for embedding. A use that just wants the actor writes no Python at all: the image
|
|
21
|
+
this package publishes renders the cards from the sidecar and runs `serve`, which is those lines
|
|
22
|
+
plus the observability wiring and the in-cluster kubeconfig that used to be copied into every repo.
|
|
23
|
+
"""
|
|
24
|
+
from .config import (CapabilityConfig, ConfigError, Peer, Report, Secret, SecretTemplate,
|
|
25
|
+
cards_path, lint, version)
|
|
26
|
+
from .deploy import DeployError
|
|
27
|
+
from .handler import HandlerError, make_orchestrate_task
|
|
28
|
+
from .instance import render_cards
|
|
29
|
+
from .peers import PeerError
|
|
30
|
+
from .pulls import PullRequestError
|
|
31
|
+
from .serve import ServeError, serve
|
|
32
|
+
from .settings import Settings, SettingsError
|
|
33
|
+
from . import conformance, correlation, deploy, instance, kubeconfig, peers, pulls, round0
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"CapabilityConfig",
|
|
37
|
+
"ConfigError",
|
|
38
|
+
"DeployError",
|
|
39
|
+
"HandlerError",
|
|
40
|
+
"Peer",
|
|
41
|
+
"PeerError",
|
|
42
|
+
"PullRequestError",
|
|
43
|
+
"Report",
|
|
44
|
+
"Secret",
|
|
45
|
+
"SecretTemplate",
|
|
46
|
+
"ServeError",
|
|
47
|
+
"Settings",
|
|
48
|
+
"SettingsError",
|
|
49
|
+
"cards_path",
|
|
50
|
+
"conformance",
|
|
51
|
+
"correlation",
|
|
52
|
+
"deploy",
|
|
53
|
+
"instance",
|
|
54
|
+
"kubeconfig",
|
|
55
|
+
"lint",
|
|
56
|
+
"make_orchestrate_task",
|
|
57
|
+
"peers",
|
|
58
|
+
"pulls",
|
|
59
|
+
"render_cards",
|
|
60
|
+
"round0",
|
|
61
|
+
"serve",
|
|
62
|
+
"version",
|
|
63
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# WHAT DATA EXISTS. `papeete-actor-data/v0`, owned by `papeete-actor-message` — a named, minimally
|
|
2
|
+
# typed dictionary. Every field here belongs to the ACTOR, not to any capability: a task, the
|
|
3
|
+
# surface two peers agreed on, and how far a run got. Nothing names a capability, a component or a
|
|
4
|
+
# peer repository, because none of those are known until a sidecar supplies them.
|
|
5
|
+
data: papeete-actor-data/v0
|
|
6
|
+
items:
|
|
7
|
+
- name: task_id
|
|
8
|
+
type: string
|
|
9
|
+
description: the TASK-NNN card id to orchestrate (e.g. "TASK-011")
|
|
10
|
+
- name: title
|
|
11
|
+
type: string
|
|
12
|
+
description: the task's short title/summary, supplied by the caller — no card lookup is performed here
|
|
13
|
+
- name: context
|
|
14
|
+
type: string
|
|
15
|
+
description: free-text elaboration beyond the title — background, constraints, links; optional
|
|
16
|
+
- name: definition_of_done
|
|
17
|
+
type: list
|
|
18
|
+
description: >-
|
|
19
|
+
the acceptance criteria this task must satisfy, as the caller states them — forwarded
|
|
20
|
+
unchanged to every peer door, never re-derived or fetched
|
|
21
|
+
- name: accepted
|
|
22
|
+
type: boolean
|
|
23
|
+
description: >-
|
|
24
|
+
whether the task was agreed, implemented, tested passing, and its pull request opened
|
|
25
|
+
- name: because
|
|
26
|
+
type: string
|
|
27
|
+
description: >-
|
|
28
|
+
why the orchestration did not succeed, in prose — the stage says where, this says what
|
|
29
|
+
- name: stage
|
|
30
|
+
type: enum
|
|
31
|
+
values: [round-0, implementation, testing, verdict]
|
|
32
|
+
description: >-
|
|
33
|
+
where a run stopped. round-0: the testing and implementation actors did not agree on an
|
|
34
|
+
acceptance surface, or one of them did not answer — nothing was built. implementation:
|
|
35
|
+
implement-task could not be reached, refused, or reported nothing to test. testing:
|
|
36
|
+
test-task could not be reached, refused, published nothing to run, or the ephemeral
|
|
37
|
+
deployment and test run failed. verdict: the tests kept failing on every attempt, or passed
|
|
38
|
+
and the pull request could not be opened
|
|
39
|
+
- name: attempts
|
|
40
|
+
type: integer
|
|
41
|
+
description: >-
|
|
42
|
+
how many implement-task -> test-task cycles this run used; 0 when round 0 stopped it
|
|
43
|
+
- name: verdict
|
|
44
|
+
type: string
|
|
45
|
+
description: >-
|
|
46
|
+
a summary of the final test verdict — the passed/total count, or the failing criteria on a
|
|
47
|
+
failure
|
|
48
|
+
- name: pr_url
|
|
49
|
+
type: string
|
|
50
|
+
description: the URL of the pull request opened on the implementation repository, once testing passed
|
|
51
|
+
- name: test_pr_url
|
|
52
|
+
type: string
|
|
53
|
+
description: >-
|
|
54
|
+
the URL of the paired pull request opened on the testing repository — titled with the same
|
|
55
|
+
[task_id] tag as pr_url's so the two are reviewed and merged together; absent if only the
|
|
56
|
+
first could be opened
|
|
57
|
+
- name: branch
|
|
58
|
+
type: string
|
|
59
|
+
description: >-
|
|
60
|
+
the branch the implementation actor's implement-task pushed to (impl/TASK-NNN) — present
|
|
61
|
+
whenever an implement-task call was accepted, whether or not testing passed
|
|
62
|
+
- name: test_branch
|
|
63
|
+
type: string
|
|
64
|
+
description: >-
|
|
65
|
+
the branch the testing actor's test-task pushed to (test/TASK-NNN) — present whenever a
|
|
66
|
+
test-task call was accepted, whether or not testing passed
|
|
67
|
+
- name: acceptance_surface
|
|
68
|
+
type: list
|
|
69
|
+
description: >-
|
|
70
|
+
the observable expectations the increment must satisfy, each with a stable id, a statement
|
|
71
|
+
of what must hold, and the handle a black-box caller reaches it through — proposed by the
|
|
72
|
+
testing actor in round 0, before anything was built. Once agreed, each expectation carries
|
|
73
|
+
the implementation actor's commitments that name it under `commitments`, and a commitment
|
|
74
|
+
naming none is appended as an entry of its own. On a round-0 failure it is the proposal as
|
|
75
|
+
made
|
|
76
|
+
- name: open_questions
|
|
77
|
+
type: list
|
|
78
|
+
description: >-
|
|
79
|
+
what the testing actor said the task does not determine — each a string, or an object with
|
|
80
|
+
`about` and `question`. Non-empty stops the run in round 0: it is for a human to answer, by
|
|
81
|
+
amending the task
|
|
82
|
+
- name: objections
|
|
83
|
+
type: list
|
|
84
|
+
description: >-
|
|
85
|
+
the implementation actor's objections to the proposed surface, one per expectation it cannot
|
|
86
|
+
meet, as its assess-task door returned them
|
|
87
|
+
- name: commitments
|
|
88
|
+
type: list
|
|
89
|
+
description: >-
|
|
90
|
+
what the implementation actor undertook to pin for the expectations it accepted, as its
|
|
91
|
+
assess-task door returned them — carried on a round-0 failure so nothing it offered is lost
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# WHAT MESSAGES EXIST. `papeete-actor-message/v1`, owned by `papeete-actor-message` — named
|
|
2
|
+
# messages, each a pure regrouping of references into the data dictionary, under an intent.
|
|
3
|
+
# Wire-agnostic by that package's own design: WHAT a message is, never HOW it is carried.
|
|
4
|
+
#
|
|
5
|
+
# The two results are OUTCOMES of one door, so the binding closes both schemas: a field exclusive
|
|
6
|
+
# to one (`pr_url`, `stage`) is what rules the other out. That is why `stage` is required on
|
|
7
|
+
# failure and absent on success, rather than optional on both.
|
|
8
|
+
message: papeete-actor-message/v1
|
|
9
|
+
messages:
|
|
10
|
+
- name: orchestrate-task-cmd
|
|
11
|
+
intent: >-
|
|
12
|
+
ask this actor to drive a TASK-NNN card for the capability it serves through the three
|
|
13
|
+
amigos round, implementation, black-box testing and retries, to a pull request
|
|
14
|
+
references: [task_id, title, definition_of_done, context]
|
|
15
|
+
optional: [context]
|
|
16
|
+
|
|
17
|
+
- name: orchestration-succeeded-result
|
|
18
|
+
intent: >-
|
|
19
|
+
report that the task was agreed, implemented, tested passing against the agreed surface, and
|
|
20
|
+
that its paired pull requests were opened
|
|
21
|
+
references: [accepted, pr_url, test_pr_url, branch, test_branch, attempts, verdict,
|
|
22
|
+
acceptance_surface]
|
|
23
|
+
optional: [test_pr_url]
|
|
24
|
+
|
|
25
|
+
- name: orchestration-failed-result
|
|
26
|
+
intent: >-
|
|
27
|
+
report where the task stopped and why, carrying whatever was known by then — including, when
|
|
28
|
+
round 0 stopped it, the questions or objections a human has to resolve
|
|
29
|
+
references: [accepted, because, stage, attempts, branch, test_branch, verdict,
|
|
30
|
+
acceptance_surface, open_questions, objections, commitments]
|
|
31
|
+
optional: [branch, test_branch, verdict, acceptance_surface, open_questions, objections,
|
|
32
|
+
commitments]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# THE DOOR. `synchronous-messaging-doors/v1` — one action, and no engine on it.
|
|
2
|
+
#
|
|
3
|
+
# WHY NO ENGINE. Every decision behind this door is a fixed rule: stop on open questions or
|
|
4
|
+
# objections, call the implementation and testing doors with the agreed surface, deploy, run, retry
|
|
5
|
+
# on a red verdict up to a fixed number of attempts, open the PRs on a green one. There is nothing
|
|
6
|
+
# to judge, so `Actor.receive()` calls the registered handler with `judged=None` and no sidecar of
|
|
7
|
+
# this actor names an engine.
|
|
8
|
+
#
|
|
9
|
+
# WHY IT IS AN ACTION. It changes the world — namespaces are created and deleted, pull requests are
|
|
10
|
+
# opened — and it is a promise to try, which is what an action is. It blocks for the whole run by
|
|
11
|
+
# this pipeline's design: no callback door on any side, and escalation is this door's own refusal
|
|
12
|
+
# with the objections, back to whoever called (ADR-FIA-0004 §5).
|
|
13
|
+
manifest: synchronous-messaging-doors/v1
|
|
14
|
+
actions:
|
|
15
|
+
- id: orchestrate-task
|
|
16
|
+
means: >-
|
|
17
|
+
the door for "drive TASK-NNN of the capability I serve, from agreement to pull request".
|
|
18
|
+
Send the task id, title, definition of done and, optionally, context — the caller supplies
|
|
19
|
+
everything, no task card is looked up here. First, round 0: I ask the testing actor's
|
|
20
|
+
propose-acceptance door what it intends to assert, naming the components my sidecar
|
|
21
|
+
declares; if it has open questions I stop, and the implementation actor is not asked. Then I
|
|
22
|
+
ask the implementation actor's assess-task door whether that surface can be delivered; if it
|
|
23
|
+
is not feasible, or it objects, I stop. Either way I hand back what they said, for a human to
|
|
24
|
+
resolve. Once they agree, I call implement-task and then test-task with the agreed
|
|
25
|
+
acceptance surface — on every attempt, not only the first — deploy the touched components
|
|
26
|
+
into an ephemeral test-<task_id> namespace, run the published test image against them, and
|
|
27
|
+
tear the namespace down. A passing verdict opens the paired pull requests on both
|
|
28
|
+
repositories and I stop. A failing verdict retries with the failing criteria as a
|
|
29
|
+
remediation context, up to a fixed number of attempts; no pull request is opened on a final
|
|
30
|
+
failure. I never merge, and I never write code or tests myself.
|
|
31
|
+
completion: >-
|
|
32
|
+
whether the task reached a passing verdict and had its pull requests opened (with the PR
|
|
33
|
+
urls, both branches, the attempts used, the verdict, and the agreed acceptance surface), or
|
|
34
|
+
did not — with why, the stage it stopped at, the attempts used, and whatever of the
|
|
35
|
+
branches, verdict and surface is known; when round 0 stopped it, also the open questions,
|
|
36
|
+
objections and commitments that did.
|
|
37
|
+
door_schema: orchestrate-task-cmd
|
|
38
|
+
completion_schema: [orchestration-succeeded-result, orchestration-failed-result]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# WHO. The actor this package defines — `papeete-actor-manifest/v0`, owned by `papeete-actor`.
|
|
2
|
+
#
|
|
3
|
+
# THIS FOLDER IS THE DEFINITION, NOT A USE. The four cards here say what a foundry task-orchestration
|
|
4
|
+
# actor IS: the data it knows, the messages it exchanges, and the one door it answers. They name no
|
|
5
|
+
# capability, because which capability an instance serves is not part of what the actor is — that
|
|
6
|
+
# is `actor-agentic-context.yaml`, supplied per use.
|
|
7
|
+
#
|
|
8
|
+
# A use's cards are rendered from these at `docker build` time (`render-cards`): the three below
|
|
9
|
+
# byte-for-byte, and this one with `name:` set to the use's own actor name and its description
|
|
10
|
+
# extended with the capability and the two peers it drives.
|
|
11
|
+
manifest: papeete-actor-manifest/v0
|
|
12
|
+
name: foundry-task-orchestration-actor
|
|
13
|
+
description: >-
|
|
14
|
+
Drives one TASK-NNN card for the business capability its sidecar names, from agreement to pull
|
|
15
|
+
request. First holds the three amigos round: asks the testing actor what it intends to assert,
|
|
16
|
+
asks the implementation actor whether that can be delivered, and stops with the open questions
|
|
17
|
+
or objections if they do not agree. Then calls implement-task and test-task with the agreed
|
|
18
|
+
acceptance surface, deploys the touched components into an ephemeral namespace, runs the
|
|
19
|
+
published test image against them, and retries with the failing criteria up to a fixed number
|
|
20
|
+
of attempts. Only on a passing verdict does it open the paired pull requests. Purely mechanical
|
|
21
|
+
coordination: it writes no code and no tests and merges nothing, so it names no engine — every
|
|
22
|
+
decision here is a fixed rule, not a session's judgement.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""`foundry-task-orchestration-actor` — the gate, and the derivation table.
|
|
2
|
+
|
|
3
|
+
Four subcommands. `lint` is what CI runs against a sidecar; `show` prints every rendering
|
|
4
|
+
`config.py` derives from the two fields that are actually written down — the peers this instance
|
|
5
|
+
will call and where, the image repositories it will deploy from, the namespace, Service URLs and
|
|
6
|
+
Secrets an attempt will create — so an operator can check all of it before a run spends a single
|
|
7
|
+
peer session, rather than by reading a running actor's logs after the fact.
|
|
8
|
+
|
|
9
|
+
`render-cards` and `serve` are what make a use's whole repository one sidecar (ADR-FTOA-0001): the
|
|
10
|
+
first writes the actor's four cards beside it from the definition in this wheel, the second boots
|
|
11
|
+
the thing. Both run inside the image this package publishes; both work in a plain venv too.
|
|
12
|
+
|
|
13
|
+
House rule: every published package in this ecosystem ships a CLI named exactly the package.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import conformance
|
|
22
|
+
from .config import CapabilityConfig, ConfigError, lint, version
|
|
23
|
+
from .instance import render_cards
|
|
24
|
+
from .serve import DEFAULT_PORT, ServeError, serve
|
|
25
|
+
from .settings import ENV, Settings, SettingsError
|
|
26
|
+
|
|
27
|
+
_REGISTRY_PLACEHOLDER = "<registry>"
|
|
28
|
+
_SAMPLE_TASK = "TASK-NNN"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cmd_lint(args: argparse.Namespace) -> int:
|
|
32
|
+
# Two gates, one command. The sidecar says which capability this use serves; the cards say
|
|
33
|
+
# which actor it claims to be. A use can be wrong about either independently.
|
|
34
|
+
report = lint(Path(args.folder))
|
|
35
|
+
conformance_report = conformance.check(Path(args.folder))
|
|
36
|
+
report.oks.extend(conformance_report.oks)
|
|
37
|
+
report.warns.extend(conformance_report.warns)
|
|
38
|
+
report.errors.extend(conformance_report.errors)
|
|
39
|
+
for warning in report.warns:
|
|
40
|
+
print(f" ! {warning}")
|
|
41
|
+
for error in report.errors:
|
|
42
|
+
print(f" FAIL {error}")
|
|
43
|
+
if not report.ok:
|
|
44
|
+
print(f"✗ {len(report.errors)} error(s)")
|
|
45
|
+
return 1
|
|
46
|
+
for line in report.oks:
|
|
47
|
+
print(f" ok {line}")
|
|
48
|
+
checked_cards = any((Path(args.folder) / name).exists() for name in conformance.CARD_FILES)
|
|
49
|
+
print("✓ sidecar and cards conform" if checked_cards else "✓ sidecar conforms")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cmd_show(args: argparse.Namespace) -> int:
|
|
54
|
+
try:
|
|
55
|
+
config = CapabilityConfig.load(Path(args.folder))
|
|
56
|
+
settings = Settings.from_env()
|
|
57
|
+
except (ConfigError, SettingsError) as e:
|
|
58
|
+
print(f" FAIL {e}")
|
|
59
|
+
return 2
|
|
60
|
+
registry = args.registry or settings.image_registry or _REGISTRY_PLACEHOLDER
|
|
61
|
+
run_id = config.run_id(_SAMPLE_TASK)
|
|
62
|
+
|
|
63
|
+
rows = [
|
|
64
|
+
("capability", config.capability),
|
|
65
|
+
("source_repo", config.source_repo),
|
|
66
|
+
("actor name", config.actor_name),
|
|
67
|
+
("registry path", config.capability_path),
|
|
68
|
+
("workload prefix", config.workload_prefix),
|
|
69
|
+
("clone prefix", config.clone_prefix(_SAMPLE_TASK)),
|
|
70
|
+
("components (round 0)", ", ".join(config.components)),
|
|
71
|
+
]
|
|
72
|
+
width = max(len(label) for label, _ in rows)
|
|
73
|
+
for label, value in rows:
|
|
74
|
+
print(f" {label:<{width}} {value}")
|
|
75
|
+
|
|
76
|
+
print("\n peers")
|
|
77
|
+
for peer in (config.implementation, config.testing):
|
|
78
|
+
url = settings.peer_url(config, peer.role)
|
|
79
|
+
override = ENV[f"{peer.role}_url"]
|
|
80
|
+
url_from = (f"${override}" if url != peer.url
|
|
81
|
+
else "sidecar" if peer.declared_url else "derived")
|
|
82
|
+
print(f" {peer.role}")
|
|
83
|
+
print(f" repo {peer.repo} ({'sidecar' if peer.declared_repo else 'derived'})")
|
|
84
|
+
print(f" url {url} ({url_from})")
|
|
85
|
+
|
|
86
|
+
print(f"\n one attempt at {_SAMPLE_TASK}")
|
|
87
|
+
print(f" namespace / product {run_id}")
|
|
88
|
+
print(f" implementation clone {config.implementation.repo}@impl/{_SAMPLE_TASK}")
|
|
89
|
+
print(f" platform stand-in "
|
|
90
|
+
f"{config.platform_folder if config.platform is not None else '(none declared)'}")
|
|
91
|
+
for component in config.components:
|
|
92
|
+
print(f" {component}")
|
|
93
|
+
print(f" image name {config.image_name(component)}")
|
|
94
|
+
print(f" image {config.image_repository(registry, component)}:<version>")
|
|
95
|
+
print(f" test image {config.image_repository(registry, component)}/tests:<version>")
|
|
96
|
+
print(f" deployment {config.prefixed(run_id, config.workload_name(component))}")
|
|
97
|
+
print(f" service url {config.service_url(run_id, component)}")
|
|
98
|
+
for secret in config.render_secrets(run_id, component):
|
|
99
|
+
print(f" secret {secret.name} ({', '.join(sorted(secret.string_data))})")
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _cmd_render_cards(args: argparse.Namespace) -> int:
|
|
104
|
+
# The step that deletes the hand copy. Run at `docker build` time, right after the sidecar is
|
|
105
|
+
# COPY'd in: from then on the cards in that folder came from a version, not from an editor.
|
|
106
|
+
try:
|
|
107
|
+
config = CapabilityConfig.load(Path(args.folder))
|
|
108
|
+
except ConfigError as e:
|
|
109
|
+
print(f" FAIL {e}")
|
|
110
|
+
return 2
|
|
111
|
+
try:
|
|
112
|
+
written = render_cards(config, Path(args.folder))
|
|
113
|
+
except FileNotFoundError as e:
|
|
114
|
+
print(f" FAIL {e}")
|
|
115
|
+
return 2
|
|
116
|
+
for path in written:
|
|
117
|
+
print(f" ok rendered {path}")
|
|
118
|
+
print(f"✓ {config.actor_name} — four cards from the definition in "
|
|
119
|
+
f"foundry-task-orchestration-actor=={version()}")
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _cmd_serve(args: argparse.Namespace) -> int:
|
|
124
|
+
# No try/except around the boot itself. A misconfigured actor that starts anyway and refuses
|
|
125
|
+
# every caller at the door is strictly worse than a pod that crash-loops with the reason on
|
|
126
|
+
# stdout, which is what an uncaught error produces here.
|
|
127
|
+
try:
|
|
128
|
+
serve(Path(args.folder), port=args.port)
|
|
129
|
+
except (ConfigError, SettingsError, ServeError) as e:
|
|
130
|
+
print(f" FAIL {e}", file=sys.stderr)
|
|
131
|
+
return 2
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
136
|
+
parser = argparse.ArgumentParser(
|
|
137
|
+
prog="foundry-task-orchestration-actor",
|
|
138
|
+
description="Inspect, validate and run one capability's task-orchestration actor.",
|
|
139
|
+
)
|
|
140
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
141
|
+
|
|
142
|
+
lint_parser = sub.add_parser(
|
|
143
|
+
"lint",
|
|
144
|
+
help="validate a sidecar against foundry-task-orchestration-actor/agentic-context/v1")
|
|
145
|
+
lint_parser.add_argument(
|
|
146
|
+
"folder", nargs="?", default=".",
|
|
147
|
+
help="the actor's folder, or the sidecar file itself (default: .)")
|
|
148
|
+
lint_parser.set_defaults(func=_cmd_lint)
|
|
149
|
+
|
|
150
|
+
show_parser = sub.add_parser(
|
|
151
|
+
"show", help="print every identifier derived from the sidecar and the environment")
|
|
152
|
+
show_parser.add_argument("folder", nargs="?", default=".")
|
|
153
|
+
show_parser.add_argument(
|
|
154
|
+
"--registry",
|
|
155
|
+
help=f"render image refs against this registry (default: $IMAGE_REGISTRY, else "
|
|
156
|
+
f"{_REGISTRY_PLACEHOLDER})")
|
|
157
|
+
show_parser.set_defaults(func=_cmd_show)
|
|
158
|
+
|
|
159
|
+
render_parser = sub.add_parser(
|
|
160
|
+
"render-cards",
|
|
161
|
+
help="write the actor's four cards into a use's folder, from the definition in this wheel")
|
|
162
|
+
render_parser.add_argument(
|
|
163
|
+
"folder", nargs="?", default=".",
|
|
164
|
+
help="the folder holding this use's actor-agentic-context.yaml (default: .)")
|
|
165
|
+
render_parser.set_defaults(func=_cmd_render_cards)
|
|
166
|
+
|
|
167
|
+
serve_parser = sub.add_parser(
|
|
168
|
+
"serve", help="boot this actor and answer its door (needs the `serve` extra)")
|
|
169
|
+
serve_parser.add_argument(
|
|
170
|
+
"folder", nargs="?", default=".",
|
|
171
|
+
help="the folder holding this use's sidecar and cards (default: .)")
|
|
172
|
+
serve_parser.add_argument(
|
|
173
|
+
"--port", type=int, default=None,
|
|
174
|
+
help=f"override $PORT (default: $PORT, else {DEFAULT_PORT})")
|
|
175
|
+
serve_parser.set_defaults(func=_cmd_serve)
|
|
176
|
+
|
|
177
|
+
return parser
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main(argv: list[str] | None = None) -> int:
|
|
181
|
+
args = build_parser().parse_args(argv)
|
|
182
|
+
return args.func(args)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
sys.exit(main())
|