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.
@@ -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())