turnkit 0.5.0 → 0.6.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +187 -5
  4. data/UPGRADE.md +57 -0
  5. data/lib/generators/turnkit/install/templates/create_turnkit_tables.rb +30 -0
  6. data/lib/generators/turnkit/install/templates/delivery.rb +7 -0
  7. data/lib/generators/turnkit/install/templates/initializer.rb +5 -0
  8. data/lib/generators/turnkit/install/templates/wait.rb +7 -0
  9. data/lib/generators/turnkit/install_generator.rb +2 -0
  10. data/lib/generators/turnkit/upgrade/templates/add_turnkit_durable_orchestration.rb +36 -0
  11. data/lib/generators/turnkit/upgrade_generator.rb +34 -0
  12. data/lib/turnkit/active_record_store.rb +124 -14
  13. data/lib/turnkit/adapters/ruby_llm.rb +14 -0
  14. data/lib/turnkit/agent.rb +26 -20
  15. data/lib/turnkit/authorization.rb +17 -0
  16. data/lib/turnkit/background.rb +281 -0
  17. data/lib/turnkit/budget.rb +4 -3
  18. data/lib/turnkit/conversation.rb +23 -1
  19. data/lib/turnkit/coordination_tools.rb +61 -0
  20. data/lib/turnkit/error.rb +3 -0
  21. data/lib/turnkit/execution_store.rb +30 -0
  22. data/lib/turnkit/id.rb +1 -0
  23. data/lib/turnkit/image_tool.rb +10 -0
  24. data/lib/turnkit/job.rb +21 -0
  25. data/lib/turnkit/memory_store.rb +106 -15
  26. data/lib/turnkit/reconciliation.rb +13 -10
  27. data/lib/turnkit/record.rb +36 -3
  28. data/lib/turnkit/run.rb +15 -0
  29. data/lib/turnkit/skill.rb +5 -4
  30. data/lib/turnkit/specialists.rb +254 -0
  31. data/lib/turnkit/store.rb +38 -9
  32. data/lib/turnkit/sub_agent_tool.rb +23 -7
  33. data/lib/turnkit/system_prompt.rb +7 -7
  34. data/lib/turnkit/tool.rb +11 -0
  35. data/lib/turnkit/tool_runner.rb +101 -45
  36. data/lib/turnkit/turn.rb +188 -57
  37. data/lib/turnkit/version.rb +1 -1
  38. data/lib/turnkit.rb +30 -0
  39. metadata +15 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3db08f0653a468f22daa74070c7b1a290f02b44beaf6d35c950f3d36d86dc224
4
- data.tar.gz: 18d3b2df593865ba53e0863d0e2b061f8744bdbbf78409b40d525c3ba47b5070
3
+ metadata.gz: d8ca23035231ccf58df8389aa2e845bb3e8874b1a23d33a8e8ffb8b793c533fe
4
+ data.tar.gz: 1f1bafaf7eb330caa119504846b9960989f116780b5ea364fdbe4e7c47869db2
5
5
  SHA512:
6
- metadata.gz: dd2216c9e2275dd737d6e1cf7dadd97053ca5afb6992939791669f29e047f3e44eb45f492a09b4494222117428baf07ffd9aa60352f9363b0a8b3eedb9c6ab54
7
- data.tar.gz: 3b8f1a45bc98506dc09a9a6703aacf1f614fb7649c3c64942c98e902aff1db9232f986aad33c077be51fec1623d82abf4506d32698b34e3079ff775b9a1eb38b
6
+ metadata.gz: dbe5569a73d9118776e207567a515642d9fc59923de34b5084499623f36798e9c826832bf35d7f8105b9272252edb6da244faf896f9509e59e571d9db7f9022f
7
+ data.tar.gz: 31b2decb3c2336eafd7f943eaff23cf149b88c67067b75c785f9cd71960349a9f89a20da5dfd1d415fb1f7e083f9e061514f359568c4a5b92fc44426a8e05fcd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 - 2026-09-06
4
+
5
+ ### Added
6
+
7
+ - Independent background submission through Active Job, registered agent reconstruction, durable messages/completion callbacks, inbox/outbox queries, and worker-free joins.
8
+ - Parallel background sub-agents with persisted parent/child lineage and ordered results; opt-in launch, send-message, and wait tools.
9
+ - Persisted execution phases, fenced claims, root-wide budget reservations, and recovery of missed enqueues and abandoned workers without replaying unknown external effects.
10
+ - PostgreSQL concurrency/process-death tests and a reversible `turnkit:upgrade` migration generator.
11
+ - Agent-scoped context and skills, extensible specialist factories, authorization hooks, and fenced cancellation with explicit descendant handling.
12
+ - Live recipe, deep-research and rare-earth policy examples, plus prepared orb dependencies and OIDC-based RubyGems releases.
13
+
14
+ ### Fixed
15
+
16
+ - Reject conflicting delivery-key reuse without disclosing another conversation's payload; authorize implicit joins and detect conversation-lane wait cycles.
17
+ - Bound inline reconciliation queries and preserve replay-safe tool keys and skill activation across recovery.
18
+ - Forward image references and normalize RubyLLM provider failures as terminal model errors instead of repeatedly retrying permanent failures.
19
+
20
+ ### Breaking
21
+
22
+ - Existing ActiveRecord installations must run `turnkit:upgrade` and migrate; turn records gain `submitted_at` and `claim_token`, plus delivery and wait tables.
23
+ - Custom stores must implement the new transactional coordination contract. Reconciled inline workers lose write authority; `stale` is no longer provisional.
24
+ - Background execution requires Active Job 7.2+ and a persistent queue backend. `async: true` remains previewable pending work; call `perform_later` to submit.
25
+
3
26
  ## 0.5.0 - 2026-07-22
4
27
 
5
28
  ### Breaking
data/README.md CHANGED
@@ -548,6 +548,21 @@ end
548
548
 
549
549
  Require an image before completion with `TurnKit::OutputPolicy.require_image`.
550
550
 
551
+ For editing or style references, override `input_images(**arguments)` and
552
+ `mask(**arguments)` in your `ImageTool` subclass, just like `prompt` and
553
+ `metadata`. Both default to `nil` and are forwarded to the image provider.
554
+ Declare the corresponding tool parameters yourself; accepted sources, reference
555
+ limits, and mask support depend on the provider. Local paths must exist on the
556
+ executing worker. Generate only when the application/user requested it; use
557
+ `ViewMediaTool` for inspection, not image generation.
558
+
559
+ Image results retain URL or base64 bytes, including in tool results. Provider
560
+ URLs may expire: use the event hook above to copy images into application-owned
561
+ storage for durable user-facing links. TurnKit does not host attachments or
562
+ automatically make generated images visually available to the next text model
563
+ call. Base64 tool results also consume prompt space; for large images, prefer a
564
+ custom tool that stores the artifact and returns its application-owned URL.
565
+
551
566
  ### Media analysis
552
567
 
553
568
  Analyze existing images, PDFs, audio, or video inside a durable turn with
@@ -727,6 +742,161 @@ puts turn.output_text
727
742
 
728
743
  Use sub-agents for isolated child conversations.
729
744
 
745
+ #### Oracle- and Librarian-style specialists
746
+
747
+ Use ordinary agents, not a second specialist runtime. Give each specialist its
748
+ own model, instructions, description (when the parent should invoke it), and
749
+ explicit tool list, then include it in the parent's `sub_agents`:
750
+
751
+ - **Oracle:** instructions for a specific unresolved decision, the evidence
752
+ already checked, constraints, and a recommendation. Supply only scoped
753
+ read/search tools; the parent owns implementation and verification.
754
+ - **Librarian:** instructions for external repository understanding and a full
755
+ evidence-backed report. Supply authenticated repository read/search, history,
756
+ diff, and issue-reading tools appropriate to the application. A role name does
757
+ not provide GitHub access. Exclude repository and issue mutation tools.
758
+ - **Painter:** use `ImageTool` for an actual image-provider call rather than a
759
+ text agent pretending to generate an image.
760
+
761
+ Children receive the explicit `task`, not the parent's conversation history or
762
+ tool list. Their complete final text, structured `output_data`, status/error,
763
+ and conversation/turn IDs return through the parent's matching tool result;
764
+ intermediate child messages remain in the child conversation. Inline calls
765
+ block; background calls use the same durable fan-out/join machinery below.
766
+
767
+ This is a context boundary, not an OS or credential sandbox. Global context
768
+ contributors and custom clients can supply additional context/capabilities;
769
+ scope those deliberately. Enforce read-only access in the supplied tools and
770
+ credentials, not merely in prompts. Likewise, enforce user approval for image
771
+ generation in application tool exposure/authorization when it is a requirement.
772
+ TurnKit does not ship Amp's private tools, repository service, or attachment host.
773
+
774
+ ### Background execution and agent messaging
775
+
776
+ Durable background execution uses Active Record/Active Job 7.2+, `ActiveRecordStore`, and a
777
+ persistent backend such as Solid Queue or Sidekiq. Configure the backend in your
778
+ application; TurnKit does not start worker processes. The database owns pending
779
+ work, while jobs are wake-up signals. `MemoryStore` is for inline use and tests.
780
+
781
+ Existing installations must run `bin/rails generate turnkit:upgrade` and migrate
782
+ before using this version. New installations use `turnkit:install` as usual.
783
+
784
+ Register agent definitions at application boot **in every web and worker
785
+ process**. Registration also registers configured sub-agents. Names must identify
786
+ the same agent configuration in every process; use Rails `to_prepare` when
787
+ definitions are reloadable. TurnKit persists IDs, model, options, and lineage,
788
+ not Ruby clients, tools, closures, or executable configuration.
789
+ Subject `to_prompt` data is snapshotted in conversation metadata for worker
790
+ reconstruction. Use live-context contributors with application-owned identifiers
791
+ when background turns need freshly loaded domain objects rather than that snapshot.
792
+
793
+ ```ruby
794
+ require "turnkit/job"
795
+
796
+ writer = TurnKit::Agent.new(name: "writer", instructions: "Draft concise copy.")
797
+ editor = TurnKit.register(TurnKit::Agent.new(
798
+ name: "editor",
799
+ sub_agents: [writer],
800
+ tools: [TurnKit::LaunchAgentTool, TurnKit::SendMessageTool, TurnKit::WaitTool]
801
+ ))
802
+
803
+ run = editor.run("Research and draft the announcement.", async: true)
804
+ run.perform_later
805
+
806
+ # Another process can reconstruct records using the registered definitions.
807
+ run = TurnKit::Run.new(TurnKit.load_turn(run.id))
808
+ run.reload.status
809
+ ```
810
+
811
+ `async: true` still means **prepare pending work**, not enqueue it. This preserves
812
+ preview-only use. `perform_later` persists submission before enqueueing and
813
+ returns immediately. Background work uses the application-wide `TurnKit.store`.
814
+ Worker clients and tools must be safe for the concurrency configured in your job
815
+ backend. Do not launch unjoined Ruby threads from tools.
816
+
817
+ In a background turn, a group of consecutive sub-agent tool calls launches
818
+ independent child jobs. The parent becomes `waiting` and releases its worker;
819
+ once all children finish, it resumes with tool results in call order. A failed
820
+ child returns a structured failure, not an implicit parent failure. Ordinary
821
+ tools remain sequential, and terminal tools prevent later calls from running.
822
+ Inline turns use the same engine but execute sub-agents synchronously.
823
+
824
+ `LaunchAgentTool` starts a configured sub-agent without waiting. Its optional
825
+ `callback: true` requests a completion message in the launching conversation.
826
+ `WaitTool` joins submitted turns without holding a worker. These coordination
827
+ tools are opt-in; the application owns authorization and should expose only
828
+ the destinations/actions appropriate for that agent.
829
+
830
+ Application code can send messages and request completion callbacks too:
831
+
832
+ ```ruby
833
+ parent = TurnKit.load_conversation(parent_conversation_id)
834
+ child = writer.run("Investigate the issue.", async: true)
835
+ child.perform_later(callback: parent)
836
+
837
+ parent.send_message(other_conversation_id, "Please check the latest results.",
838
+ key: "request-123:check-results")
839
+ parent.inbox
840
+ parent.outbox
841
+
842
+ # Attach dependencies before enqueueing, avoiding a race with an eager worker.
843
+ summary = editor.run("Summarize the joined results.", async: true)
844
+ summary.wait_for(child).perform_later
845
+ ```
846
+
847
+ Delivery keys are globally unique idempotency keys. Retrying the same key returns
848
+ the original delivery; it does not replace its payload. Inbox, outbox, and
849
+ completion callbacks use the same persisted delivery rows. Delivery appends one
850
+ message and records the need for a continuation transactionally. Messages to a
851
+ running or waiting conversation do not change its current input snapshot: they
852
+ are consumed by a later turn. Background turns in one conversation execute
853
+ serially. Use `TurnKit.load_conversation` to address a conversation independently
854
+ of a worker's fenced execution context.
855
+
856
+ Application-level `wait_for` supplies joined results as turn-local input before
857
+ the first model call. Attach waits only to pending turns. Wait targets must be
858
+ submitted or finished, and must not form dependency cycles. Waiting for unfinished
859
+ work in the same conversation is rejected because execution there is serial.
860
+
861
+ #### Recovery and operational requirements
862
+
863
+ Schedule `TurnKit::ReconcileJob` using your backend's recurring-job facility
864
+ (for example every minute). This repairs missed enqueues, undelivered messages,
865
+ ready joins, and abandoned workers. `TurnKit.reconcile_stale!` also recovers
866
+ submitted work while retaining the inline stale-turn behavior described below.
867
+
868
+ - Claims are fenced: after revocation or completion, an old execution cannot
869
+ persist another model response, tool result, heartbeat, or completion.
870
+ - Recovery reuses committed model responses and tool results. An interrupted
871
+ model request may be reissued and charged again by the provider.
872
+ - Started external tool calls without results become `interrupted`, with unknown
873
+ outcomes. They are **not replayed**. The built-in durable coordination tools can
874
+ resume using their existing child IDs/delivery keys.
875
+ - Waiting is not a stale heartbeat. Root timeout still applies, including queue
876
+ and wait time from initial submission. It is a cooperative limit, not a hard
877
+ process kill. Use provider/tool timeouts for blocking external calls.
878
+ - Root iteration and tool-count limits are reserved under a database lock across
879
+ parallel children. Spend limits use observed cost; already-running requests can
880
+ overshoot them.
881
+ - `on_event` remains an inline observation hook, not a durable callback. Background
882
+ infrastructure exceptions reach the job backend rather than being converted
883
+ into success. Monitor failed jobs and run reconciliation.
884
+ - TurnKit fences its own persistence, not arbitrary external side effects. Tools
885
+ should use `context.turn.store` for execution-owned TurnKit writes. External
886
+ services need their own idempotency keys where appropriate.
887
+
888
+ The PostgreSQL integration suite exercises real row locks and worker-process
889
+ death. Run it against a dedicated test database:
890
+
891
+ ```sh
892
+ TURNKIT_TEST_DATABASE_URL=postgresql:///turnkit_test bundle exec rake test
893
+ ```
894
+
895
+ It creates/rebuilds only `turnkit_test_*` and `turnkit_upgrade_test_*` test tables.
896
+ Without the variable, database tests explicitly skip. Custom stores must implement
897
+ the transactional `Store` contract, including reentrant `atomic`, deliveries,
898
+ waits, and submitted-turn queries; a mutex alone is not cross-process durability.
899
+
730
900
  ### Context Compaction
731
901
 
732
902
  Disable compaction:
@@ -791,7 +961,8 @@ process). Run this periodically:
791
961
  TurnKit.reconcile_stale!
792
962
  ```
793
963
 
794
- Reconciliation atomically marks pending and running turns whose last heartbeat
964
+ For unsubmitted inline work, reconciliation atomically marks pending and running
965
+ turns whose last heartbeat
795
966
  is older than `TurnKit.timeout` as `stale`, so it never overwrites a turn that
796
967
  was concurrently claimed, heartbeated, or completed. Each stale turn's
797
968
  unfinished tool executions become `interrupted`, and a synthetic error tool
@@ -799,10 +970,10 @@ result is appended for any unresolved tool call so the conversation can be
799
970
  continued. TurnKit never reruns an interrupted tool — whether its side effect
800
971
  happened is unknown, so the continued model is told not to assume either way.
801
972
 
802
- Reconciliation does not cancel or reassign the underlying process. If the
803
- original worker is still alive (its heartbeats were merely late), it continues
804
- over the synthetic interrupted result and later replaces `stale` with its
805
- actual `completed` or `failed` outcome treat `turn.stale` as provisional.
973
+ Reconciliation revokes the original worker's commit authority. A late worker
974
+ cannot replace `stale` with its own outcome. It does not kill the underlying
975
+ process or undo external side effects. Continue an inline stale conversation
976
+ with a new turn; submitted background work is resumed automatically.
806
977
 
807
978
  ## Options
808
979
 
@@ -877,6 +1048,11 @@ agent = TurnKit::Agent.new(
877
1048
 
878
1049
  ## Upgrading
879
1050
 
1051
+ See [runtime hardening](docs/runtime-hardening.md) for scoped context, authorization,
1052
+ cancellation, cycle-safe waits, replay-safe effects and bounded maintenance, and
1053
+ [specialists and skills](docs/specialists.md) for the extensible Oracle, Librarian
1054
+ and Painter factories and skill-owned tools.
1055
+
880
1056
  See the [0.4.2 Upgrade Guide](UPGRADE_TO_0_4_2.md) for the full API migration checklist.
881
1057
 
882
1058
  Rails installs from older versions may need `output_data` for structured output,
@@ -906,6 +1082,12 @@ find lib test examples -type f -name '*.rb' -print0 | xargs -0 ruby -c
906
1082
 
907
1083
  Open a pull request.
908
1084
 
1085
+ ## Releasing
1086
+
1087
+ Maintainers: follow [the release guide](docs/releasing.md) for RubyGems trusted
1088
+ publishing setup, version and lockfile updates, and the explicit tag-push procedure.
1089
+ Pushing a `v*` tag triggers tests and publication through the `release` environment.
1090
+
909
1091
  ## License
910
1092
 
911
1093
  Use this gem under the MIT License.
data/UPGRADE.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Upgrade Guide
2
2
 
3
+ ## 0.6.0: Runtime hardening
4
+
5
+ Configure global context/skills at boot, before constructing agents; defaults are now
6
+ snapshotted. Use agent-scoped `context_contributors:` and run-scoped `context:` for requests.
7
+ Specialist factories disable global inheritance and validate read-only skill tools.
8
+ Skill-owned tools activate only after successful loading in the current turn.
9
+
10
+ Configure `TurnKit.authorization_policy` and pass authenticated `principal:` values for
11
+ multi-user applications. The default remains trusted application code. Submitted cancellation
12
+ is terminal and fenced, with explicit `descendants: :retain` or `:cascade` semantics.
13
+
14
+ PostgreSQL is required for ActiveRecord wait-graph locking. Custom stores must implement
15
+ atomic graph operations and bounded actionable queries. Earlier durable-schema adopters
16
+ need the two indexes in [runtime hardening](docs/runtime-hardening.md); new install/upgrade
17
+ migrations already contain them. That guide also documents stable tool idempotency keys,
18
+ opt-in replay safety and the limits of external-effect recovery.
19
+
20
+ Delivery key reuse with different routing or payload now raises `ToolError` instead of
21
+ returning the original message. Implicit subagent joins honor `:wait` policy. Cycle
22
+ checks conservatively include all unfinished turns in a conversation's serial lane.
23
+ Custom stores also need `list_stale_inline_turns(before:, limit:)`; public inline
24
+ reconciliation now drains bounded batches rather than all history in one call.
25
+
26
+ ## 0.6.0: Durable background execution
27
+
28
+ Stop existing workers before upgrading: older workers do not respect claim
29
+ fencing. Generate and apply the additive migration, then restart the application
30
+ and workers together:
31
+
32
+ ```sh
33
+ bin/rails generate turnkit:upgrade
34
+ bin/rails db:migrate
35
+ ```
36
+
37
+ Use `--table-prefix your_prefix` if your installation uses custom table names.
38
+ The generator adds delivery/wait models and a reversible migration; it does not
39
+ replace existing models or delete conversation history. For custom model names,
40
+ pass `delivery_class:` and `wait_class:` to `ActiveRecordStore` alongside the
41
+ existing model-class options.
42
+
43
+ Background execution requires Active Job 7.2+ with a persistent queue adapter,
44
+ agent registration at boot in every worker, and a recurring
45
+ `TurnKit::ReconcileJob`. See [background execution](README.md#background-execution-and-agent-messaging)
46
+ for examples and operational semantics. `async: true` does not enqueue work.
47
+
48
+ Custom stores need reentrant, rollback-capable `atomic(conversation_id)` with
49
+ cross-process locking for durable use, submitted-turn queries, delivery CRUD
50
+ with a unique idempotency key, and idempotent wait relations. Turn statuses now
51
+ include `waiting`; turn records carry `submitted_at` and `claim_token`.
52
+ The base store implements inline stale reconciliation using these primitives.
53
+
54
+ Do not rely on a late worker overwriting a reconciled stale turn. Its claim is
55
+ now revoked. Execution-owned writes must go through `context.turn.store`, not a
56
+ global/raw store. Background limits are rooted in persisted run configuration;
57
+ timeout includes queue and wait time after submission. Inline use needs no job
58
+ backend, but ActiveRecord schemas and custom store contracts still need updating.
59
+
3
60
  ## 0.4.2
4
61
 
5
62
  TurnKit 0.4.2 is a pre-1.0 surface cleanup. Update these call sites before upgrading:
@@ -31,6 +31,8 @@ class CreateTurnkitTables < ActiveRecord::Migration[7.1]
31
31
  t.json :error
32
32
  t.text :output_text
33
33
  t.json :output_data
34
+ t.datetime :submitted_at
35
+ t.string :claim_token
34
36
  t.datetime :started_at
35
37
  t.datetime :heartbeat_at
36
38
  t.datetime :completed_at
@@ -40,6 +42,34 @@ class CreateTurnkitTables < ActiveRecord::Migration[7.1]
40
42
  t.index :conversation_uid
41
43
  t.index :root_turn_uid
42
44
  t.index [ :status, :heartbeat_at ]
45
+ t.index [ :status, :submitted_at, :updated_at ], name: "index_<%= table_prefix %>_turns_on_maintenance"
46
+ end
47
+
48
+ create_table :<%= table_prefix %>_deliveries do |t|
49
+ t.string :uid, null: false
50
+ t.string :source_conversation_uid, null: false
51
+ t.string :destination_conversation_uid, null: false
52
+ t.string :source_turn_uid
53
+ t.string :key, null: false
54
+ t.json :payload, null: false, default: {}
55
+ t.string :message_uid
56
+ t.datetime :delivered_at
57
+ t.timestamps
58
+
59
+ t.index :uid, unique: true
60
+ t.index :key, unique: true
61
+ t.index [ :source_conversation_uid, :created_at ]
62
+ t.index [ :destination_conversation_uid, :delivered_at ]
63
+ t.index [ :delivered_at, :created_at ], name: "index_<%= table_prefix %>_deliveries_on_pending"
64
+ end
65
+
66
+ create_table :<%= table_prefix %>_waits do |t|
67
+ t.string :turn_uid, null: false
68
+ t.string :target_turn_uid, null: false
69
+ t.timestamps
70
+
71
+ t.index [ :turn_uid, :target_turn_uid ], unique: true
72
+ t.index :target_turn_uid
43
73
  end
44
74
 
45
75
  create_table :<%= table_prefix %>_messages do |t|
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Turnkit
4
+ class Delivery < ApplicationRecord
5
+ self.table_name = "<%= table_prefix %>_deliveries"
6
+ end
7
+ end
@@ -8,6 +8,11 @@
8
8
  # TurnKit::ActiveRecordStore.new(conversation_class: "My::Conversation", ...)
9
9
  TurnKit.store = TurnKit::ActiveRecordStore.new
10
10
 
11
+ # Background work: require "turnkit/job", configure a persistent Active Job
12
+ # backend, register agents with TurnKit.register at boot in every process,
13
+ # and schedule TurnKit::ReconcileJob in your recurring-job facility.
14
+ # async: true only prepares a run; perform_later submits it.
15
+
11
16
  # TurnKit.default_model = "claude-sonnet-4-5"
12
17
  # TurnKit.max_iterations = 25
13
18
  # TurnKit.timeout = 300
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Turnkit
4
+ class Wait < ApplicationRecord
5
+ self.table_name = "<%= table_prefix %>_waits"
6
+ end
7
+ end
@@ -23,6 +23,8 @@ module TurnKit
23
23
  template "turn.rb", "app/models/turnkit/turn.rb"
24
24
  template "message.rb", "app/models/turnkit/message.rb"
25
25
  template "tool_execution.rb", "app/models/turnkit/tool_execution.rb"
26
+ template "delivery.rb", "app/models/turnkit/delivery.rb"
27
+ template "wait.rb", "app/models/turnkit/wait.rb"
26
28
  end
27
29
 
28
30
  def create_ai_directories
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddTurnkitDurableOrchestration < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :<%= table_prefix %>_turns, :submitted_at, :datetime
6
+ add_column :<%= table_prefix %>_turns, :claim_token, :string
7
+ add_index :<%= table_prefix %>_turns, [ :status, :submitted_at, :updated_at ], name: "index_<%= table_prefix %>_turns_on_maintenance"
8
+
9
+ create_table :<%= table_prefix %>_deliveries do |t|
10
+ t.string :uid, null: false
11
+ t.string :source_conversation_uid, null: false
12
+ t.string :destination_conversation_uid, null: false
13
+ t.string :source_turn_uid
14
+ t.string :key, null: false
15
+ t.json :payload, null: false, default: {}
16
+ t.string :message_uid
17
+ t.datetime :delivered_at
18
+ t.timestamps
19
+
20
+ t.index :uid, unique: true
21
+ t.index :key, unique: true
22
+ t.index [ :source_conversation_uid, :created_at ]
23
+ t.index [ :destination_conversation_uid, :delivered_at ]
24
+ t.index [ :delivered_at, :created_at ], name: "index_<%= table_prefix %>_deliveries_on_pending"
25
+ end
26
+
27
+ create_table :<%= table_prefix %>_waits do |t|
28
+ t.string :turn_uid, null: false
29
+ t.string :target_turn_uid, null: false
30
+ t.timestamps
31
+
32
+ t.index [ :turn_uid, :target_turn_uid ], unique: true
33
+ t.index :target_turn_uid
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module TurnKit
7
+ module Generators
8
+ class UpgradeGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ namespace "turnkit:upgrade"
12
+ source_root File.expand_path("upgrade/templates", __dir__)
13
+ source_paths << File.expand_path("install/templates", __dir__)
14
+
15
+ class_option :table_prefix, type: :string, default: "turnkit", desc: "Database table prefix."
16
+
17
+ def copy_models
18
+ template "delivery.rb", "app/models/turnkit/delivery.rb"
19
+ template "wait.rb", "app/models/turnkit/wait.rb"
20
+ end
21
+
22
+ def copy_migration
23
+ migration_template "add_turnkit_durable_orchestration.rb", "db/migrate/add_turnkit_durable_orchestration.rb"
24
+ end
25
+
26
+ def self.next_migration_number(dirname)
27
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
28
+ end
29
+
30
+ private
31
+ def table_prefix = options[:table_prefix]
32
+ end
33
+ end
34
+ end